@aurabase/aurabase-js 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/.changeset/config.json +11 -0
  2. package/CHANGELOG.md +30 -0
  3. package/LICENSE +21 -0
  4. package/README.md +833 -0
  5. package/package.json +90 -0
  6. package/packages/ai/LICENSE +21 -0
  7. package/packages/ai/README.md +106 -0
  8. package/packages/ai/dist/index.cjs +229 -0
  9. package/packages/ai/dist/index.d.cts +139 -0
  10. package/packages/ai/dist/index.d.ts +139 -0
  11. package/packages/ai/dist/index.js +226 -0
  12. package/packages/ai/node_modules/.bin/esbuild +19 -0
  13. package/packages/ai/node_modules/.bin/tsc +22 -0
  14. package/packages/ai/node_modules/.bin/tsserver +22 -0
  15. package/packages/ai/node_modules/.bin/tsup +22 -0
  16. package/packages/ai/node_modules/.bin/tsup-node +22 -0
  17. package/packages/ai/node_modules/.bin/vite +22 -0
  18. package/packages/ai/node_modules/.bin/vitest +22 -0
  19. package/packages/ai/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  20. package/packages/ai/package.json +61 -0
  21. package/packages/ai/src/AiService.ts +226 -0
  22. package/packages/ai/src/index.ts +18 -0
  23. package/packages/ai/src/sse.ts +141 -0
  24. package/packages/ai/src/types.ts +30 -0
  25. package/packages/ai/tests/chat-stream.test.ts +151 -0
  26. package/packages/ai/tests/rag-namespaces.test.ts +54 -0
  27. package/packages/ai/tests/sse.test.ts +69 -0
  28. package/packages/ai/tsconfig.json +5 -0
  29. package/packages/ai/tsup.config.ts +11 -0
  30. package/packages/aurabase/LICENSE +21 -0
  31. package/packages/aurabase/README.md +64 -0
  32. package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.cts +77 -0
  33. package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.ts +77 -0
  34. package/packages/aurabase/dist/index.cjs +237 -0
  35. package/packages/aurabase/dist/index.d.cts +46 -0
  36. package/packages/aurabase/dist/index.d.ts +46 -0
  37. package/packages/aurabase/dist/index.js +177 -0
  38. package/packages/aurabase/dist/ssr.cjs +224 -0
  39. package/packages/aurabase/dist/ssr.d.cts +65 -0
  40. package/packages/aurabase/dist/ssr.d.ts +65 -0
  41. package/packages/aurabase/dist/ssr.js +214 -0
  42. package/packages/aurabase/node_modules/.bin/esbuild +19 -0
  43. package/packages/aurabase/node_modules/.bin/tsc +22 -0
  44. package/packages/aurabase/node_modules/.bin/tsserver +22 -0
  45. package/packages/aurabase/node_modules/.bin/tsup +22 -0
  46. package/packages/aurabase/node_modules/.bin/tsup-node +22 -0
  47. package/packages/aurabase/node_modules/.bin/vite +22 -0
  48. package/packages/aurabase/node_modules/.bin/vitest +22 -0
  49. package/packages/aurabase/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  50. package/packages/aurabase/package.json +80 -0
  51. package/packages/aurabase/src/AuraClient.ts +237 -0
  52. package/packages/aurabase/src/index.ts +152 -0
  53. package/packages/aurabase/src/ssr.ts +156 -0
  54. package/packages/aurabase/tests/ai-streaming-rag-e2e.test.ts +82 -0
  55. package/packages/aurabase/tests/ai.test.ts +111 -0
  56. package/packages/aurabase/tests/auth-admin.test.ts +515 -0
  57. package/packages/aurabase/tests/auth-e2e.test.ts +135 -0
  58. package/packages/aurabase/tests/auth-hardening.test.ts +206 -0
  59. package/packages/aurabase/tests/auth.test.ts +947 -0
  60. package/packages/aurabase/tests/database.test.ts +230 -0
  61. package/packages/aurabase/tests/db-types-e2e.test.ts +217 -0
  62. package/packages/aurabase/tests/dispose.test.ts +23 -0
  63. package/packages/aurabase/tests/foreign-token.e2e.test.ts +129 -0
  64. package/packages/aurabase/tests/functions.test.ts +107 -0
  65. package/packages/aurabase/tests/integration.test.ts +413 -0
  66. package/packages/aurabase/tests/live-production-validation.js +241 -0
  67. package/packages/aurabase/tests/live-sdk-production-validation.js +293 -0
  68. package/packages/aurabase/tests/mocks.ts +139 -0
  69. package/packages/aurabase/tests/notifications.test.ts +314 -0
  70. package/packages/aurabase/tests/observability-backup.test.ts +186 -0
  71. package/packages/aurabase/tests/observability-hook.test.ts +130 -0
  72. package/packages/aurabase/tests/production-robustness.test.ts +268 -0
  73. package/packages/aurabase/tests/public-api-surface.test.ts +53 -0
  74. package/packages/aurabase/tests/query-builder.test.ts +329 -0
  75. package/packages/aurabase/tests/readme-smoke-e2e.test.ts +231 -0
  76. package/packages/aurabase/tests/realtime-filter-e2e.test.ts +344 -0
  77. package/packages/aurabase/tests/refresh-locks.test.ts +210 -0
  78. package/packages/aurabase/tests/retry-option.test.ts +17 -0
  79. package/packages/aurabase/tests/rpc-e2e.test.ts +254 -0
  80. package/packages/aurabase/tests/rs256-local-e2e.test.ts +114 -0
  81. package/packages/aurabase/tests/rt-alias-public-e2e.test.ts +235 -0
  82. package/packages/aurabase/tests/rt-http-broadcast-e2e.test.ts +124 -0
  83. package/packages/aurabase/tests/service-role-guard.test.ts +99 -0
  84. package/packages/aurabase/tests/ssr.test.ts +91 -0
  85. package/packages/aurabase/tests/storage-backend-e2e.test.ts +481 -0
  86. package/packages/aurabase/tests/storage-sdk-parity-e2e.test.ts +104 -0
  87. package/packages/aurabase/tests/storage-transform-e2e.test.ts +684 -0
  88. package/packages/aurabase/tests/storage.test.ts +55 -0
  89. package/packages/aurabase/tests/types/client-generic.test-d.ts +79 -0
  90. package/packages/aurabase/tests/types/docs-snippets.test-d.ts +571 -0
  91. package/packages/aurabase/tests/types/error-code.test-d.ts +53 -0
  92. package/packages/aurabase/tsconfig.json +5 -0
  93. package/packages/aurabase/tsconfig.test-d.json +5 -0
  94. package/packages/aurabase/tsup.config.ts +11 -0
  95. package/packages/aurabase/vitest.config.ts +16 -0
  96. package/packages/aurabase/vitest.e2e.config.ts +9 -0
  97. package/packages/auth/LICENSE +21 -0
  98. package/packages/auth/README.md +215 -0
  99. package/packages/auth/dist/index.cjs +1175 -0
  100. package/packages/auth/dist/index.d.cts +397 -0
  101. package/packages/auth/dist/index.d.ts +397 -0
  102. package/packages/auth/dist/index.js +1170 -0
  103. package/packages/auth/node_modules/.bin/esbuild +19 -0
  104. package/packages/auth/node_modules/.bin/tsc +22 -0
  105. package/packages/auth/node_modules/.bin/tsserver +22 -0
  106. package/packages/auth/node_modules/.bin/tsup +22 -0
  107. package/packages/auth/node_modules/.bin/tsup-node +22 -0
  108. package/packages/auth/node_modules/.bin/vite +22 -0
  109. package/packages/auth/node_modules/.bin/vitest +22 -0
  110. package/packages/auth/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  111. package/packages/auth/package.json +61 -0
  112. package/packages/auth/src/AuthAdminService.ts +149 -0
  113. package/packages/auth/src/AuthService.ts +1166 -0
  114. package/packages/auth/src/SessionManager.ts +92 -0
  115. package/packages/auth/src/index.ts +17 -0
  116. package/packages/auth/src/jwks.ts +337 -0
  117. package/packages/auth/src/pkce.ts +27 -0
  118. package/packages/auth/src/types.ts +89 -0
  119. package/packages/auth/tests/auth-lifecycle.test.ts +149 -0
  120. package/packages/auth/tests/detect-session.test.ts +361 -0
  121. package/packages/auth/tests/getclaims.test.ts +780 -0
  122. package/packages/auth/tests/is-mfa-challenge.test.ts +27 -0
  123. package/packages/auth/tests/password-recovery.test.ts +124 -0
  124. package/packages/auth/tests/pkce.test.ts +192 -0
  125. package/packages/auth/tests/session-manager.test.ts +103 -0
  126. package/packages/auth/tests/set-session.test.ts +139 -0
  127. package/packages/auth/tests/verify-otp.test.ts +153 -0
  128. package/packages/auth/tsconfig.json +5 -0
  129. package/packages/auth/tsup.config.ts +11 -0
  130. package/packages/core/LICENSE +21 -0
  131. package/packages/core/README.md +132 -0
  132. package/packages/core/dist/index.cjs +695 -0
  133. package/packages/core/dist/index.d.cts +1137 -0
  134. package/packages/core/dist/index.d.ts +1137 -0
  135. package/packages/core/dist/index.js +673 -0
  136. package/packages/core/node_modules/.bin/esbuild +22 -0
  137. package/packages/core/node_modules/.bin/tsc +22 -0
  138. package/packages/core/node_modules/.bin/tsserver +22 -0
  139. package/packages/core/node_modules/.bin/tsup +22 -0
  140. package/packages/core/node_modules/.bin/tsup-node +22 -0
  141. package/packages/core/node_modules/.bin/vite +22 -0
  142. package/packages/core/node_modules/.bin/vitest +22 -0
  143. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  144. package/packages/core/package.json +57 -0
  145. package/packages/core/src/AuraError.ts +89 -0
  146. package/packages/core/src/BaseService.ts +44 -0
  147. package/packages/core/src/client.ts +34 -0
  148. package/packages/core/src/cookie-utils.ts +42 -0
  149. package/packages/core/src/fetch.ts +435 -0
  150. package/packages/core/src/index.ts +22 -0
  151. package/packages/core/src/retry.ts +63 -0
  152. package/packages/core/src/serialize.ts +6 -0
  153. package/packages/core/src/stores/BaseAuthStore.ts +164 -0
  154. package/packages/core/src/stores/LocalAuthStore.ts +199 -0
  155. package/packages/core/src/types.ts +1042 -0
  156. package/packages/core/tests/auth-store.test.ts +300 -0
  157. package/packages/core/tests/error-code.test.ts +61 -0
  158. package/packages/core/tests/error.test.ts +24 -0
  159. package/packages/core/tests/fetch.test.ts +286 -0
  160. package/packages/core/tests/mocks.ts +139 -0
  161. package/packages/core/tests/observability.test.ts +86 -0
  162. package/packages/core/tests/refresh-hardening.test.ts +164 -0
  163. package/packages/core/tests/refresh-observability.test.ts +111 -0
  164. package/packages/core/tests/retry.test.ts +106 -0
  165. package/packages/core/tests/serialize.test.ts +12 -0
  166. package/packages/core/tests/timeout.test.ts +79 -0
  167. package/packages/core/tsconfig.json +5 -0
  168. package/packages/core/tsup.config.ts +11 -0
  169. package/packages/db/LICENSE +21 -0
  170. package/packages/db/README.md +152 -0
  171. package/packages/db/VENDORED.md +93 -0
  172. package/packages/db/dist/index.cjs +544 -0
  173. package/packages/db/dist/index.d.cts +1111 -0
  174. package/packages/db/dist/index.d.ts +1111 -0
  175. package/packages/db/dist/index.js +541 -0
  176. package/packages/db/node_modules/.bin/esbuild +19 -0
  177. package/packages/db/node_modules/.bin/tsc +22 -0
  178. package/packages/db/node_modules/.bin/tsserver +22 -0
  179. package/packages/db/node_modules/.bin/tsup +22 -0
  180. package/packages/db/node_modules/.bin/tsup-node +22 -0
  181. package/packages/db/node_modules/.bin/vite +22 -0
  182. package/packages/db/node_modules/.bin/vitest +22 -0
  183. package/packages/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  184. package/packages/db/package.json +62 -0
  185. package/packages/db/src/DatabaseService.ts +170 -0
  186. package/packages/db/src/QueryBuilder.ts +830 -0
  187. package/packages/db/src/index.ts +2 -0
  188. package/packages/db/src/pgrst-types/common/common.ts +86 -0
  189. package/packages/db/src/pgrst-types/common/rpc.ts +151 -0
  190. package/packages/db/src/pgrst-types/feature-flags.ts +20 -0
  191. package/packages/db/src/pgrst-types/types.ts +134 -0
  192. package/packages/db/src/select-query-parser/parser.ts +479 -0
  193. package/packages/db/src/select-query-parser/result.ts +553 -0
  194. package/packages/db/src/select-query-parser/types.ts +132 -0
  195. package/packages/db/src/select-query-parser/utils.ts +707 -0
  196. package/packages/db/tests/filters.test.ts +81 -0
  197. package/packages/db/tests/mutations-filtered.e2e.test.ts +132 -0
  198. package/packages/db/tests/mutations-filtered.test.ts +188 -0
  199. package/packages/db/tests/order.test.ts +45 -0
  200. package/packages/db/tests/rpc.test.ts +41 -0
  201. package/packages/db/tests/scale/build-large-schema.ts +94 -0
  202. package/packages/db/tests/scale/scale-compile.test.ts +167 -0
  203. package/packages/db/tests/schema.test.ts +77 -0
  204. package/packages/db/tests/single-accept.e2e.test.ts +123 -0
  205. package/packages/db/tests/single.test.ts +211 -0
  206. package/packages/db/tests/types/filters.test-d.ts +52 -0
  207. package/packages/db/tests/types/fixtures.ts +40 -0
  208. package/packages/db/tests/types/from-views.test-d.ts +18 -0
  209. package/packages/db/tests/types/mutations.test-d.ts +23 -0
  210. package/packages/db/tests/types/rpc.test-d.ts +60 -0
  211. package/packages/db/tests/types/schema-shape.test-d.ts +8 -0
  212. package/packages/db/tests/types/select-inference.test-d.ts +38 -0
  213. package/packages/db/tests/vendored/fixtures.ts +83 -0
  214. package/packages/db/tests/vendored/get-result.test-d.ts +75 -0
  215. package/packages/db/tsconfig.json +5 -0
  216. package/packages/db/tsconfig.test-d.json +5 -0
  217. package/packages/db/tsup.config.ts +11 -0
  218. package/packages/db/vitest.config.ts +8 -0
  219. package/packages/functions/LICENSE +21 -0
  220. package/packages/functions/README.md +96 -0
  221. package/packages/functions/dist/index.cjs +173 -0
  222. package/packages/functions/dist/index.d.cts +81 -0
  223. package/packages/functions/dist/index.d.ts +81 -0
  224. package/packages/functions/dist/index.js +168 -0
  225. package/packages/functions/node_modules/.bin/esbuild +19 -0
  226. package/packages/functions/node_modules/.bin/tsc +22 -0
  227. package/packages/functions/node_modules/.bin/tsserver +22 -0
  228. package/packages/functions/node_modules/.bin/tsup +22 -0
  229. package/packages/functions/node_modules/.bin/tsup-node +22 -0
  230. package/packages/functions/node_modules/.bin/vite +22 -0
  231. package/packages/functions/node_modules/.bin/vitest +22 -0
  232. package/packages/functions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  233. package/packages/functions/package.json +60 -0
  234. package/packages/functions/src/FunctionsService.ts +188 -0
  235. package/packages/functions/src/errors.ts +70 -0
  236. package/packages/functions/src/index.ts +6 -0
  237. package/packages/functions/tests/invoke.test.ts +97 -0
  238. package/packages/functions/tests/response-type-errors.test.ts +135 -0
  239. package/packages/functions/tsconfig.json +5 -0
  240. package/packages/functions/tsup.config.ts +11 -0
  241. package/packages/gen/LICENSE +21 -0
  242. package/packages/gen/README.md +84 -0
  243. package/packages/gen/dist/cli.cjs +334 -0
  244. package/packages/gen/dist/cli.d.cts +1 -0
  245. package/packages/gen/dist/cli.d.ts +1 -0
  246. package/packages/gen/dist/cli.js +332 -0
  247. package/packages/gen/dist/index.cjs +257 -0
  248. package/packages/gen/dist/index.d.cts +12 -0
  249. package/packages/gen/dist/index.d.ts +12 -0
  250. package/packages/gen/dist/index.js +255 -0
  251. package/packages/gen/node_modules/.bin/esbuild +19 -0
  252. package/packages/gen/node_modules/.bin/tsc +22 -0
  253. package/packages/gen/node_modules/.bin/tsserver +22 -0
  254. package/packages/gen/node_modules/.bin/tsup +22 -0
  255. package/packages/gen/node_modules/.bin/tsup-node +22 -0
  256. package/packages/gen/node_modules/.bin/vite +22 -0
  257. package/packages/gen/node_modules/.bin/vitest +22 -0
  258. package/packages/gen/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  259. package/packages/gen/package.json +66 -0
  260. package/packages/gen/src/cli.ts +67 -0
  261. package/packages/gen/src/generate.ts +0 -0
  262. package/packages/gen/src/index.ts +2 -0
  263. package/packages/gen/tests/compile.test.ts +213 -0
  264. package/packages/gen/tests/e2e.test.ts +130 -0
  265. package/packages/gen/tests/generate.test.ts +396 -0
  266. package/packages/gen/tsconfig.json +5 -0
  267. package/packages/gen/tsup.config.ts +11 -0
  268. package/packages/notifications/LICENSE +21 -0
  269. package/packages/notifications/README.md +134 -0
  270. package/packages/notifications/dist/index.cjs +128 -0
  271. package/packages/notifications/dist/index.d.cts +81 -0
  272. package/packages/notifications/dist/index.d.ts +81 -0
  273. package/packages/notifications/dist/index.js +125 -0
  274. package/packages/notifications/node_modules/.bin/esbuild +19 -0
  275. package/packages/notifications/node_modules/.bin/tsc +22 -0
  276. package/packages/notifications/node_modules/.bin/tsserver +22 -0
  277. package/packages/notifications/node_modules/.bin/tsup +22 -0
  278. package/packages/notifications/node_modules/.bin/tsup-node +22 -0
  279. package/packages/notifications/node_modules/.bin/vite +22 -0
  280. package/packages/notifications/node_modules/.bin/vitest +22 -0
  281. package/packages/notifications/package.json +60 -0
  282. package/packages/notifications/src/NotificationsService.ts +230 -0
  283. package/packages/notifications/src/index.ts +19 -0
  284. package/packages/notifications/tsconfig.json +5 -0
  285. package/packages/notifications/tsup.config.ts +11 -0
  286. package/packages/realtime/LICENSE +21 -0
  287. package/packages/realtime/README.md +183 -0
  288. package/packages/realtime/dist/index.cjs +977 -0
  289. package/packages/realtime/dist/index.d.cts +259 -0
  290. package/packages/realtime/dist/index.d.ts +259 -0
  291. package/packages/realtime/dist/index.js +973 -0
  292. package/packages/realtime/node_modules/.bin/esbuild +19 -0
  293. package/packages/realtime/node_modules/.bin/tsc +22 -0
  294. package/packages/realtime/node_modules/.bin/tsserver +22 -0
  295. package/packages/realtime/node_modules/.bin/tsup +22 -0
  296. package/packages/realtime/node_modules/.bin/tsup-node +22 -0
  297. package/packages/realtime/node_modules/.bin/vite +22 -0
  298. package/packages/realtime/node_modules/.bin/vitest +22 -0
  299. package/packages/realtime/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  300. package/packages/realtime/package.json +60 -0
  301. package/packages/realtime/src/AuraChannel.ts +485 -0
  302. package/packages/realtime/src/HeartbeatManager.ts +73 -0
  303. package/packages/realtime/src/RealtimeService.ts +505 -0
  304. package/packages/realtime/src/columnFilter.ts +301 -0
  305. package/packages/realtime/src/index.ts +4 -0
  306. package/packages/realtime/tests/ack-routing.test.ts +81 -0
  307. package/packages/realtime/tests/column-filter.test.ts +144 -0
  308. package/packages/realtime/tests/dispatch.test.ts +191 -0
  309. package/packages/realtime/tests/heartbeat.test.ts +67 -0
  310. package/packages/realtime/tests/http-broadcast.test.ts +68 -0
  311. package/packages/realtime/tests/realtime-token.test.ts +43 -0
  312. package/packages/realtime/tests/reconnect-race.test.ts +149 -0
  313. package/packages/realtime/tests/ref-id.test.ts +34 -0
  314. package/packages/realtime/tests/set-auth.test.ts +70 -0
  315. package/packages/realtime/tsconfig.json +5 -0
  316. package/packages/realtime/tsup.config.ts +11 -0
  317. package/packages/storage/LICENSE +21 -0
  318. package/packages/storage/README.md +141 -0
  319. package/packages/storage/dist/index.cjs +333 -0
  320. package/packages/storage/dist/index.d.cts +185 -0
  321. package/packages/storage/dist/index.d.ts +185 -0
  322. package/packages/storage/dist/index.js +329 -0
  323. package/packages/storage/node_modules/.bin/esbuild +19 -0
  324. package/packages/storage/node_modules/.bin/tsc +22 -0
  325. package/packages/storage/node_modules/.bin/tsserver +22 -0
  326. package/packages/storage/node_modules/.bin/tsup +22 -0
  327. package/packages/storage/node_modules/.bin/tsup-node +22 -0
  328. package/packages/storage/node_modules/.bin/vite +22 -0
  329. package/packages/storage/node_modules/.bin/vitest +22 -0
  330. package/packages/storage/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  331. package/packages/storage/package.json +60 -0
  332. package/packages/storage/src/DownloadBuilder.ts +60 -0
  333. package/packages/storage/src/StorageService.ts +323 -0
  334. package/packages/storage/src/index.ts +2 -0
  335. package/packages/storage/src/path.ts +35 -0
  336. package/packages/storage/src/transform.ts +15 -0
  337. package/packages/storage/tests/download.test.ts +64 -0
  338. package/packages/storage/tests/parity.test.ts +154 -0
  339. package/packages/storage/tests/path.test.ts +87 -0
  340. package/packages/storage/tests/transform.test.ts +79 -0
  341. package/packages/storage/tsconfig.json +5 -0
  342. package/packages/storage/tsup.config.ts +11 -0
  343. package/pnpm-workspace.yaml +24 -0
  344. package/tsconfig.base.json +15 -0
@@ -0,0 +1,947 @@
1
+ // ---------------------------------------------------------------------------
2
+ // AuthService tests
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
6
+ import { AuraClient } from '../src/AuraClient'
7
+ import { BaseAuthStore } from '@aurabase/core'
8
+ import { FetchMock, expiredJwt, fakeTokenPair, fakeUser, validJwt } from './mocks'
9
+
10
+ const BASE = 'http://localhost:8080'
11
+ const PROJECT = 'test-project'
12
+ const PREFIX = `${BASE}/v1/auth/${PROJECT}`
13
+
14
+ let fetchMock: FetchMock
15
+ let client: AuraClient
16
+
17
+ beforeEach(() => {
18
+ fetchMock = new FetchMock()
19
+ fetchMock.init()
20
+ client = new AuraClient(BASE, PROJECT, {
21
+ auth: { persistSession: false },
22
+ })
23
+ })
24
+
25
+ afterEach(() => {
26
+ fetchMock.restore()
27
+ fetchMock.clearMocks()
28
+ })
29
+
30
+ // ---------------------------------------------------------------------------
31
+ describe('AuthService.signUp', () => {
32
+ it('saves tokens on successful registration', async () => {
33
+ const tp = fakeTokenPair()
34
+ fetchMock.on({
35
+ method: 'POST',
36
+ url: `${PREFIX}/register`,
37
+ replyCode: 200,
38
+ replyBody: { data: tp },
39
+ })
40
+
41
+ const res = await client.auth.signUp({ email: 'a@b.com', password: 'secret' })
42
+
43
+ expect(res.error).toBeNull()
44
+ expect(res.data).toBeDefined()
45
+ expect(client.authStore.token).toBe(tp.access_token)
46
+ expect(client.authStore.refreshToken).toBe(tp.refresh_token)
47
+ expect(client.authStore.user?.email).toBe('test@example.com')
48
+ })
49
+
50
+ it('returns error on 400', async () => {
51
+ fetchMock.on({
52
+ method: 'POST',
53
+ url: `${PREFIX}/register`,
54
+ replyCode: 400,
55
+ replyBody: { error: { code: 'invalid_request', message: 'Email taken' } },
56
+ })
57
+
58
+ const res = await client.auth.signUp({ email: 'a@b.com', password: 'x' })
59
+
60
+ expect(res.error).toBeDefined()
61
+ expect(res.error!.code).toBe('invalid_request')
62
+ expect(client.authStore.token).toBeNull()
63
+ })
64
+ })
65
+
66
+ // ---------------------------------------------------------------------------
67
+ describe('AuthService.signInWithPassword', () => {
68
+ it('saves tokens on success', async () => {
69
+ const tp = fakeTokenPair()
70
+ fetchMock.on({
71
+ method: 'POST',
72
+ url: `${PREFIX}/login`,
73
+ replyCode: 200,
74
+ replyBody: { data: tp },
75
+ })
76
+
77
+ const res = await client.auth.signInWithPassword({ email: 'a@b.com', password: 'pass' })
78
+
79
+ expect(res.error).toBeNull()
80
+ expect(client.authStore.token).toBe(tp.access_token)
81
+ expect(client.authStore.user?.id).toBe('user-1')
82
+ })
83
+
84
+ it('detects MFA challenge', async () => {
85
+ const mfa = { mfa_required: true, mfa_token: 'mfa_tok_123' }
86
+ fetchMock.on({
87
+ method: 'POST',
88
+ url: `${PREFIX}/login`,
89
+ replyCode: 200,
90
+ replyBody: { data: mfa },
91
+ })
92
+
93
+ const events: string[] = []
94
+ client.auth.onAuthStateChange((payload) => {
95
+ events.push(payload.event)
96
+ })
97
+
98
+ const res = await client.auth.signInWithPassword({ email: 'a@b.com', password: 'pass' })
99
+
100
+ expect(res.data).toBeDefined()
101
+ expect((res.data as any).mfa_required).toBe(true)
102
+ // Token should NOT be saved for MFA challenge
103
+ expect(client.authStore.token).toBeNull()
104
+ expect(events).toContain('MFA_CHALLENGE')
105
+ })
106
+
107
+ it('MFA_CHALLENGE porte mfaToken + mfaSetupRequired', async () => {
108
+ fetchMock.on({
109
+ method: 'POST',
110
+ url: `${PREFIX}/login`,
111
+ replyCode: 200,
112
+ replyBody: { data: { mfa_required: true, mfa_token: 'MT', mfa_setup_required: true } },
113
+ })
114
+
115
+ const payloads: any[] = []
116
+ client.auth.onAuthStateChange((p) => payloads.push(p))
117
+
118
+ await client.auth.signInWithPassword({ email: 'e', password: 'p' })
119
+
120
+ const c = payloads.find((p) => p.event === 'MFA_CHALLENGE')
121
+ expect(c).toBeDefined()
122
+ expect(c.mfaToken).toBe('MT')
123
+ expect(c.mfaSetupRequired).toBe(true)
124
+ })
125
+ })
126
+
127
+ // ---------------------------------------------------------------------------
128
+ describe('AuthService.signOut', () => {
129
+ it('clears store on signOut', async () => {
130
+ // Pre-populate the store
131
+ const tp = fakeTokenPair()
132
+ client.authStore.save(tp.access_token, tp.refresh_token, tp.user)
133
+
134
+ fetchMock.on({
135
+ method: 'POST',
136
+ url: `${PREFIX}/logout`,
137
+ replyCode: 200,
138
+ replyBody: { data: null },
139
+ })
140
+
141
+ const res = await client.auth.signOut()
142
+
143
+ expect(res.error).toBeNull()
144
+ // LOT 3 (I5) : `logout` backend (`services/aura-auth/src/handlers/auth.rs::logout`)
145
+ // renvoie réellement `ApiResponse::<()>::empty()` — `fetch.ts` honore maintenant ce
146
+ // `null` explicite (`'data' in body`) au lieu de retomber sur l'enveloppe entière.
147
+ expect(res.data).toBeNull()
148
+ expect(client.authStore.token).toBeNull()
149
+ expect(client.authStore.refreshToken).toBeNull()
150
+ expect(client.authStore.user).toBeNull()
151
+ })
152
+
153
+ it('does not refresh before logout', async () => {
154
+ const tp = fakeTokenPair({ access_token: expiredJwt(), refresh_token: 'rt_old' })
155
+ client.authStore.save(tp.access_token, tp.refresh_token, tp.user)
156
+
157
+ fetchMock.on({
158
+ method: 'POST',
159
+ url: `${PREFIX}/logout`,
160
+ replyCode: 200,
161
+ replyBody: { data: null },
162
+ })
163
+
164
+ const res = await client.auth.signOut()
165
+
166
+ expect(res.error).toBeNull()
167
+ expect(fetchMock.calls).toHaveLength(1)
168
+ expect(fetchMock.calls[0].url).toBe(`${PREFIX}/logout`)
169
+ expect((fetchMock.calls[0].body as any).refresh_token).toBe('rt_old')
170
+ })
171
+
172
+ it('clears local store without network when no session exists', async () => {
173
+ const res = await client.auth.signOut()
174
+
175
+ expect(res.error).toBeNull()
176
+ expect(fetchMock.calls).toHaveLength(0)
177
+ })
178
+ })
179
+
180
+ // ---------------------------------------------------------------------------
181
+ describe('AuthService.refreshSession', () => {
182
+ it('updates store with new tokens', async () => {
183
+ const oldTp = fakeTokenPair()
184
+ client.authStore.save(oldTp.access_token, oldTp.refresh_token, oldTp.user)
185
+
186
+ const newTp = fakeTokenPair({
187
+ access_token: validJwt(),
188
+ refresh_token: 'rt_new',
189
+ user: fakeUser({ email: 'new@example.com' }),
190
+ })
191
+ fetchMock.on({
192
+ method: 'POST',
193
+ url: `${PREFIX}/refresh`,
194
+ replyCode: 200,
195
+ replyBody: { data: newTp },
196
+ })
197
+
198
+ const events: string[] = []
199
+ client.auth.onAuthStateChange((p) => events.push(p.event))
200
+
201
+ const res = await client.auth.refreshSession()
202
+
203
+ expect(res.error).toBeNull()
204
+ expect(client.authStore.refreshToken).toBe('rt_new')
205
+ expect(client.authStore.user?.email).toBe('new@example.com')
206
+ expect(events).toContain('TOKEN_REFRESHED')
207
+ })
208
+
209
+ it('returns a local error when no refresh token exists', async () => {
210
+ const res = await client.auth.refreshSession()
211
+
212
+ expect(res.data).toBeNull()
213
+ expect(res.error?.code).toBe('no_session')
214
+ expect(fetchMock.calls).toHaveLength(0)
215
+ })
216
+ })
217
+
218
+ // ---------------------------------------------------------------------------
219
+ describe('AuthService.sessions', () => {
220
+ it('sends X-Refresh-Token when listing sessions', async () => {
221
+ const tp = fakeTokenPair({ refresh_token: 'rt_current' })
222
+ client.authStore.save(tp.access_token, tp.refresh_token, tp.user)
223
+ fetchMock.on({
224
+ method: 'GET',
225
+ url: `${PREFIX}/sessions`,
226
+ replyCode: 200,
227
+ replyBody: { data: [] },
228
+ })
229
+
230
+ const res = await client.auth.listSessions()
231
+
232
+ expect(res.error).toBeNull()
233
+ expect(fetchMock.calls[0].headers.get('x-refresh-token')).toBe('rt_current')
234
+ })
235
+
236
+ it('clears store after global revoke succeeds', async () => {
237
+ const tp = fakeTokenPair({ refresh_token: 'rt_global' })
238
+ client.authStore.save(tp.access_token, tp.refresh_token, tp.user)
239
+ fetchMock.on({
240
+ method: 'POST',
241
+ url: `${PREFIX}/logout`,
242
+ replyCode: 200,
243
+ replyBody: { data: null },
244
+ })
245
+
246
+ const res = await client.auth.revokeAllSessions()
247
+
248
+ expect(res.error).toBeNull()
249
+ expect(client.authStore.token).toBeNull()
250
+ expect((fetchMock.calls[0].body as any).scope).toBe('global')
251
+ expect((fetchMock.calls[0].body as any).refresh_token).toBe('rt_global')
252
+ })
253
+ })
254
+
255
+ // ---------------------------------------------------------------------------
256
+ describe('AuthService.getUser', () => {
257
+ it('returns user from /me endpoint', async () => {
258
+ const tp = fakeTokenPair()
259
+ client.authStore.save(tp.access_token, tp.refresh_token, tp.user)
260
+
261
+ const user = fakeUser({ email: 'me@example.com' })
262
+ fetchMock.on({
263
+ method: 'GET',
264
+ url: `${PREFIX}/user`,
265
+ replyCode: 200,
266
+ replyBody: { data: user },
267
+ })
268
+
269
+ const res = await client.auth.getUser()
270
+
271
+ expect(res.error).toBeNull()
272
+ expect(res.data?.email).toBe('me@example.com')
273
+ })
274
+
275
+ it('returns 401 when not authenticated', async () => {
276
+ fetchMock.on({
277
+ method: 'GET',
278
+ url: `${PREFIX}/user`,
279
+ replyCode: 401,
280
+ replyBody: { error: { code: 'unauthorized', message: 'Not authenticated' } },
281
+ })
282
+
283
+ const res = await client.auth.getUser()
284
+
285
+ expect(res.error).toBeDefined()
286
+ expect(res.error!.code).toBe('unauthorized')
287
+ })
288
+ })
289
+
290
+ // ---------------------------------------------------------------------------
291
+ describe('AuthService.onAuthStateChange', () => {
292
+ it('fires callback on sign in and sign out', async () => {
293
+ const tp = fakeTokenPair()
294
+ fetchMock.on({
295
+ method: 'POST',
296
+ url: `${PREFIX}/login`,
297
+ replyCode: 200,
298
+ replyBody: { data: tp },
299
+ })
300
+ fetchMock.on({
301
+ method: 'POST',
302
+ url: `${PREFIX}/logout`,
303
+ replyCode: 200,
304
+ replyBody: { data: null },
305
+ })
306
+
307
+ const events: string[] = []
308
+ const { unsubscribe } = client.auth.onAuthStateChange((p) => {
309
+ events.push(p.event)
310
+ })
311
+
312
+ await client.auth.signInWithPassword({ email: 'a@b.com', password: 'pass' })
313
+ await client.auth.signOut()
314
+
315
+ expect(events).toContain('SIGNED_IN')
316
+ expect(events).toContain('SIGNED_OUT')
317
+
318
+ unsubscribe()
319
+ })
320
+
321
+ it('unsubscribe stops further events', async () => {
322
+ const tp = fakeTokenPair()
323
+ fetchMock.on({
324
+ method: 'POST',
325
+ url: `${PREFIX}/login`,
326
+ replyCode: 200,
327
+ replyBody: { data: tp },
328
+ })
329
+
330
+ const events: string[] = []
331
+ const { unsubscribe } = client.auth.onAuthStateChange((p) => {
332
+ events.push(p.event)
333
+ })
334
+
335
+ await client.auth.signInWithPassword({ email: 'a@b.com', password: 'pass' })
336
+ expect(events.length).toBeGreaterThan(0)
337
+
338
+ const countBefore = events.length
339
+ unsubscribe()
340
+
341
+ // Manually trigger another change -- should not fire
342
+ client.authStore.clear()
343
+ expect(events.length).toBe(countBefore)
344
+ })
345
+ })
346
+
347
+ // ---------------------------------------------------------------------------
348
+ describe('AuthService.signInAnonymously', () => {
349
+ it('saves anonymous session', async () => {
350
+ const tp = fakeTokenPair({
351
+ user: fakeUser({ is_anonymous: true, email: null }),
352
+ })
353
+ fetchMock.on({
354
+ method: 'POST',
355
+ url: `${PREFIX}/anonymous`,
356
+ replyCode: 200,
357
+ replyBody: { data: tp },
358
+ })
359
+
360
+ const res = await client.auth.signInAnonymously()
361
+
362
+ expect(res.error).toBeNull()
363
+ expect(client.authStore.user?.is_anonymous).toBe(true)
364
+ expect(client.authStore.token).toBe(tp.access_token)
365
+ })
366
+ })
367
+
368
+ // ---------------------------------------------------------------------------
369
+ describe('AuthService.signInWithMagicLink', () => {
370
+ it('sends POST /magic-link with email', async () => {
371
+ fetchMock.on({
372
+ method: 'POST',
373
+ url: `${PREFIX}/magic-link`,
374
+ replyCode: 200,
375
+ replyBody: { data: { message: 'Magic link sent' } },
376
+ })
377
+
378
+ const res = await client.auth.signInWithMagicLink({ email: 'a@b.com' })
379
+
380
+ expect(res.error).toBeNull()
381
+ expect(fetchMock.calls[0].url).toBe(`${PREFIX}/magic-link`)
382
+ expect(fetchMock.calls[0].method).toBe('POST')
383
+ expect((fetchMock.calls[0].body as any).email).toBe('a@b.com')
384
+ })
385
+
386
+ it('returns error on 400', async () => {
387
+ fetchMock.on({
388
+ method: 'POST',
389
+ url: `${PREFIX}/magic-link`,
390
+ replyCode: 400,
391
+ replyBody: { error: { code: 'invalid_email', message: 'Invalid email' } },
392
+ })
393
+
394
+ const res = await client.auth.signInWithMagicLink({ email: 'bad' })
395
+
396
+ expect(res.error).toBeDefined()
397
+ expect(res.error!.code).toBe('invalid_email')
398
+ })
399
+ })
400
+
401
+ // ---------------------------------------------------------------------------
402
+ describe('AuthService.signInWithOAuth', () => {
403
+ it('renvoie { url } et envoie redirect_uri (pas redirect_to)', async () => {
404
+ const { data, error } = await client.auth.signInWithOAuth('github', {
405
+ redirectTo: 'http://app/cb',
406
+ })
407
+ // No fetch call should be made — OAuth start is a redirect
408
+ expect(fetchMock.calls.length).toBe(0)
409
+ expect(error).toBeNull()
410
+ expect(data?.url).toContain('/oauth/github/start')
411
+ expect(data?.url).toContain('redirect_uri=')
412
+ expect(data?.url).not.toContain('redirect_to=')
413
+ })
414
+ })
415
+
416
+ // ---------------------------------------------------------------------------
417
+ describe('AuthService.signInWithEmailOtp', () => {
418
+ it('sends POST /email-otp/send with email', async () => {
419
+ fetchMock.on({
420
+ method: 'POST',
421
+ url: `${PREFIX}/email-otp/send`,
422
+ replyCode: 200,
423
+ replyBody: { data: { message: 'OTP sent' } },
424
+ })
425
+
426
+ const res = await client.auth.signInWithEmailOtp('otp@example.com')
427
+
428
+ expect(res.error).toBeNull()
429
+ expect(fetchMock.calls[0].url).toBe(`${PREFIX}/email-otp/send`)
430
+ expect((fetchMock.calls[0].body as any).email).toBe('otp@example.com')
431
+ })
432
+ })
433
+
434
+ // ---------------------------------------------------------------------------
435
+ describe('AuthService.verifyEmailOtp', () => {
436
+ it('saves tokens on success', async () => {
437
+ const tp = fakeTokenPair()
438
+ fetchMock.on({
439
+ method: 'POST',
440
+ url: `${PREFIX}/email-otp/verify`,
441
+ replyCode: 200,
442
+ replyBody: { data: tp },
443
+ })
444
+
445
+ const res = await client.auth.verifyEmailOtp('otp@example.com', '123456')
446
+
447
+ expect(res.error).toBeNull()
448
+ expect(client.authStore.token).toBe(tp.access_token)
449
+ expect((fetchMock.calls[0].body as any).email).toBe('otp@example.com')
450
+ expect((fetchMock.calls[0].body as any).code).toBe('123456')
451
+ })
452
+
453
+ it('returns error on invalid code', async () => {
454
+ fetchMock.on({
455
+ method: 'POST',
456
+ url: `${PREFIX}/email-otp/verify`,
457
+ replyCode: 400,
458
+ replyBody: { error: { code: 'invalid_otp', message: 'Code invalide' } },
459
+ })
460
+
461
+ const res = await client.auth.verifyEmailOtp('otp@example.com', '000000')
462
+
463
+ expect(res.error).toBeDefined()
464
+ expect(res.error!.code).toBe('invalid_otp')
465
+ expect(client.authStore.token).toBeNull()
466
+ })
467
+ })
468
+
469
+ // ---------------------------------------------------------------------------
470
+ describe('AuthService.signInWithSmsOtp', () => {
471
+ it('sends POST /sms/send with phone', async () => {
472
+ fetchMock.on({
473
+ method: 'POST',
474
+ url: `${PREFIX}/sms/send`,
475
+ replyCode: 200,
476
+ replyBody: { data: { message: 'SMS sent' } },
477
+ })
478
+
479
+ const res = await client.auth.signInWithSmsOtp('+33612345678')
480
+
481
+ expect(res.error).toBeNull()
482
+ expect((fetchMock.calls[0].body as any).phone).toBe('+33612345678')
483
+ })
484
+ })
485
+
486
+ // ---------------------------------------------------------------------------
487
+ describe('AuthService.verifySmsOtp', () => {
488
+ it('saves tokens on success', async () => {
489
+ const tp = fakeTokenPair()
490
+ fetchMock.on({
491
+ method: 'POST',
492
+ url: `${PREFIX}/sms/verify`,
493
+ replyCode: 200,
494
+ replyBody: { data: tp },
495
+ })
496
+
497
+ const res = await client.auth.verifySmsOtp('+33612345678', '654321')
498
+
499
+ expect(res.error).toBeNull()
500
+ expect(client.authStore.token).toBe(tp.access_token)
501
+ expect((fetchMock.calls[0].body as any).phone).toBe('+33612345678')
502
+ expect((fetchMock.calls[0].body as any).code).toBe('654321')
503
+ })
504
+ })
505
+
506
+ // ---------------------------------------------------------------------------
507
+ describe('AuthService.verifyEmail', () => {
508
+ it('saves tokens after email verification', async () => {
509
+ const tp = fakeTokenPair()
510
+ fetchMock.on({
511
+ method: 'POST',
512
+ url: `${PREFIX}/verify-email`,
513
+ replyCode: 200,
514
+ replyBody: { data: tp },
515
+ })
516
+
517
+ const res = await client.auth.verifyEmail('verify-token-123')
518
+
519
+ expect(res.error).toBeNull()
520
+ expect(client.authStore.token).toBe(tp.access_token)
521
+ expect((fetchMock.calls[0].body as any).token).toBe('verify-token-123')
522
+ })
523
+ })
524
+
525
+ // ---------------------------------------------------------------------------
526
+ describe('AuthService.resendVerificationEmail', () => {
527
+ it('sends POST /resend-verification', async () => {
528
+ fetchMock.on({
529
+ method: 'POST',
530
+ url: `${PREFIX}/resend-verification`,
531
+ replyCode: 200,
532
+ replyBody: { data: { message: 'Email sent' } },
533
+ })
534
+
535
+ const res = await client.auth.resendVerificationEmail('a@b.com')
536
+
537
+ expect(res.error).toBeNull()
538
+ expect(fetchMock.calls[0].url).toBe(`${PREFIX}/resend-verification`)
539
+ expect((fetchMock.calls[0].body as any).email).toBe('a@b.com')
540
+ })
541
+
542
+ it('returns a local error when email is missing', async () => {
543
+ const res = await client.auth.resendVerificationEmail()
544
+
545
+ expect(res.data).toBeNull()
546
+ expect(res.error?.code).toBe('missing_email')
547
+ expect(fetchMock.calls).toHaveLength(0)
548
+ })
549
+ })
550
+
551
+ // ---------------------------------------------------------------------------
552
+ describe('AuthService.updateUser', () => {
553
+ it('sends PUT /user with body and updates store', async () => {
554
+ const tp = fakeTokenPair()
555
+ client.authStore.save(tp.access_token, tp.refresh_token, tp.user)
556
+
557
+ const updatedUser = fakeUser({ email: 'updated@example.com' })
558
+ fetchMock.on({
559
+ method: 'PUT',
560
+ url: `${PREFIX}/user`,
561
+ replyCode: 200,
562
+ replyBody: { data: updatedUser },
563
+ })
564
+
565
+ const res = await client.auth.updateUser({ email: 'updated@example.com' })
566
+
567
+ expect(res.error).toBeNull()
568
+ expect(client.authStore.user?.email).toBe('updated@example.com')
569
+ expect(fetchMock.calls[0].method).toBe('PUT')
570
+ })
571
+ })
572
+
573
+ // ---------------------------------------------------------------------------
574
+ describe('AuthService.resetPasswordForEmail', () => {
575
+ it('sends POST /forgot-password with email', async () => {
576
+ fetchMock.on({
577
+ method: 'POST',
578
+ url: `${PREFIX}/forgot-password`,
579
+ replyCode: 200,
580
+ replyBody: { data: { message: 'Email sent' } },
581
+ })
582
+
583
+ const res = await client.auth.resetPasswordForEmail({ email: 'reset@example.com' })
584
+
585
+ expect(res.error).toBeNull()
586
+ expect((fetchMock.calls[0].body as any).email).toBe('reset@example.com')
587
+ })
588
+ })
589
+
590
+ // ---------------------------------------------------------------------------
591
+ describe('AuthService.updatePassword', () => {
592
+ it('sends PUT /user with password fields', async () => {
593
+ const tp = fakeTokenPair()
594
+ client.authStore.save(tp.access_token, tp.refresh_token, tp.user)
595
+
596
+ const updatedUser = fakeUser()
597
+ fetchMock.on({
598
+ method: 'PUT',
599
+ url: `${PREFIX}/user`,
600
+ replyCode: 200,
601
+ replyBody: { data: updatedUser },
602
+ })
603
+
604
+ const res = await client.auth.updatePassword('newpass123', 'oldpass')
605
+
606
+ expect(res.error).toBeNull()
607
+ expect((fetchMock.calls[0].body as any).password).toBe('newpass123')
608
+ expect((fetchMock.calls[0].body as any).current_password).toBe('oldpass')
609
+ })
610
+
611
+ it('réussi → clear store + SIGNED_OUT (le backend révoque toutes les sessions)', async () => {
612
+ const tp = fakeTokenPair()
613
+ client.authStore.save(tp.access_token, tp.refresh_token, tp.user)
614
+
615
+ fetchMock.on({
616
+ method: 'PUT',
617
+ url: `${PREFIX}/user`,
618
+ replyCode: 200,
619
+ replyBody: { data: fakeUser() },
620
+ })
621
+
622
+ const events: string[] = []
623
+ client.auth.onAuthStateChange(({ event }) => {
624
+ if (event !== 'INITIAL_SESSION') events.push(event)
625
+ })
626
+
627
+ const res = await client.auth.updatePassword('New123!', 'Old123!')
628
+
629
+ expect(res.error).toBeNull()
630
+ expect(client.authStore.token).toBeNull()
631
+ expect(client.authStore.refreshToken).toBeNull()
632
+ expect(events).toContain('SIGNED_OUT')
633
+ })
634
+
635
+ it('échec → ne déconnecte pas', async () => {
636
+ const tp = fakeTokenPair()
637
+ client.authStore.save(tp.access_token, tp.refresh_token, tp.user)
638
+
639
+ fetchMock.on({
640
+ method: 'PUT',
641
+ url: `${PREFIX}/user`,
642
+ replyCode: 400,
643
+ replyBody: { error: { code: 'invalid_password', message: 'Wrong current password' } },
644
+ })
645
+
646
+ const res = await client.auth.updatePassword('New123!', 'WrongOld')
647
+
648
+ expect(res.error).toBeDefined()
649
+ expect(client.authStore.token).toBe(tp.access_token)
650
+ })
651
+ })
652
+
653
+ // ---------------------------------------------------------------------------
654
+ describe('AuthService.mfaEnroll', () => {
655
+ it('returns qr_code and secret', async () => {
656
+ fetchMock.on({
657
+ method: 'POST',
658
+ url: `${PREFIX}/mfa/setup`,
659
+ replyCode: 200,
660
+ replyBody: { data: { qr_code: 'data:image/png;base64,abc', secret: 'JBSWY3DPEHPK3PXP' } },
661
+ })
662
+
663
+ const res = await client.auth.mfaEnroll()
664
+
665
+ expect(res.error).toBeNull()
666
+ expect((res.data as any).qr_code).toContain('data:image')
667
+ expect((res.data as any).secret).toBe('JBSWY3DPEHPK3PXP')
668
+ })
669
+ })
670
+
671
+ // ---------------------------------------------------------------------------
672
+ describe('AuthService.mfaVerifySetup', () => {
673
+ it('returns recovery_codes', async () => {
674
+ const codes = ['AAAA-BBBB', 'CCCC-DDDD', 'EEEE-FFFF']
675
+ fetchMock.on({
676
+ method: 'POST',
677
+ url: `${PREFIX}/mfa/setup/verify`,
678
+ replyCode: 200,
679
+ replyBody: { data: { recovery_codes: codes } },
680
+ })
681
+
682
+ const res = await client.auth.mfaVerifySetup('123456')
683
+
684
+ expect(res.error).toBeNull()
685
+ expect((res.data as any).recovery_codes).toEqual(codes)
686
+ expect((fetchMock.calls[0].body as any).code).toBe('123456')
687
+ })
688
+ })
689
+
690
+ // ---------------------------------------------------------------------------
691
+ describe('AuthService.mfaVerify', () => {
692
+ it('saves tokens after MFA verification', async () => {
693
+ const tp = fakeTokenPair()
694
+ fetchMock.on({
695
+ method: 'POST',
696
+ url: `${PREFIX}/mfa/verify-login`,
697
+ replyCode: 200,
698
+ replyBody: { data: tp },
699
+ })
700
+
701
+ const res = await client.auth.mfaVerify({ mfa_token: 'mfa_tok', code: '654321' })
702
+
703
+ expect(res.error).toBeNull()
704
+ expect(client.authStore.token).toBe(tp.access_token)
705
+ expect(client.authStore.user?.id).toBe('user-1')
706
+ })
707
+ })
708
+
709
+ // ---------------------------------------------------------------------------
710
+ describe('AuthService.mfaUnenroll', () => {
711
+ it('sends POST /mfa/disable with password', async () => {
712
+ fetchMock.on({
713
+ method: 'POST',
714
+ url: `${PREFIX}/mfa/disable`,
715
+ replyCode: 200,
716
+ replyBody: { data: { message: 'MFA disabled' } },
717
+ })
718
+
719
+ const res = await client.auth.mfaUnenroll('mypassword')
720
+
721
+ expect(res.error).toBeNull()
722
+ expect((fetchMock.calls[0].body as any).password).toBe('mypassword')
723
+ })
724
+ })
725
+
726
+ // ---------------------------------------------------------------------------
727
+ describe('AuthService.listIdentities', () => {
728
+ it('returns identity list', async () => {
729
+ const identities = [{ id: 'id-1', provider: 'google', provider_id: 'g123' }]
730
+ fetchMock.on({
731
+ method: 'GET',
732
+ url: `${PREFIX}/user/identities`,
733
+ replyCode: 200,
734
+ replyBody: { data: identities },
735
+ })
736
+
737
+ const res = await client.auth.listIdentities()
738
+
739
+ expect(res.error).toBeNull()
740
+ expect(res.data).toHaveLength(1)
741
+ expect(fetchMock.calls[0].method).toBe('GET')
742
+ })
743
+ })
744
+
745
+ // ---------------------------------------------------------------------------
746
+ describe('AuthService.linkIdentity', () => {
747
+ it('sends POST to link provider', async () => {
748
+ fetchMock.on({
749
+ method: 'POST',
750
+ url: `${PREFIX}/user/identities/link/github`,
751
+ replyCode: 200,
752
+ replyBody: { data: { url: 'https://github.com/login/oauth', provider: 'github' } },
753
+ })
754
+
755
+ const res = await client.auth.linkIdentity('github')
756
+
757
+ expect(res.error).toBeNull()
758
+ expect((res.data as any).provider).toBe('github')
759
+ })
760
+ })
761
+
762
+ // ---------------------------------------------------------------------------
763
+ describe('AuthService.unlinkIdentity', () => {
764
+ it('sends DELETE to unlink identity', async () => {
765
+ fetchMock.on({
766
+ method: 'DELETE',
767
+ url: `${PREFIX}/user/identities/id-42`,
768
+ replyCode: 200,
769
+ replyBody: { data: { message: 'Unlinked' } },
770
+ })
771
+
772
+ const res = await client.auth.unlinkIdentity('id-42')
773
+
774
+ expect(res.error).toBeNull()
775
+ expect(fetchMock.calls[0].method).toBe('DELETE')
776
+ expect(fetchMock.calls[0].url).toContain('id-42')
777
+ })
778
+ })
779
+
780
+ // ---------------------------------------------------------------------------
781
+ describe('AuthService.confirmEmailChange', () => {
782
+ it('sends POST with token', async () => {
783
+ const user = fakeUser({ email: 'newemail@example.com' })
784
+ fetchMock.on({
785
+ method: 'POST',
786
+ url: `${PREFIX}/user/confirm-email-change`,
787
+ replyCode: 200,
788
+ replyBody: { data: user },
789
+ })
790
+
791
+ const res = await client.auth.confirmEmailChange('change-token-abc')
792
+
793
+ expect(res.error).toBeNull()
794
+ expect((fetchMock.calls[0].body as any).token).toBe('change-token-abc')
795
+ })
796
+ })
797
+
798
+ // ---------------------------------------------------------------------------
799
+ describe('AuthService.signInWithIdToken', () => {
800
+ it('saves tokens on native mobile sign-in', async () => {
801
+ const tp = fakeTokenPair()
802
+ fetchMock.on({
803
+ method: 'POST',
804
+ url: `${PREFIX}/token`,
805
+ replyCode: 200,
806
+ replyBody: { data: tp },
807
+ })
808
+
809
+ const res = await client.auth.signInWithIdToken('google', 'google-id-token-abc')
810
+
811
+ expect(res.error).toBeNull()
812
+ expect(client.authStore.token).toBe(tp.access_token)
813
+ expect((fetchMock.calls[0].body as any).provider).toBe('google')
814
+ expect((fetchMock.calls[0].body as any).id_token).toBe('google-id-token-abc')
815
+ })
816
+ })
817
+
818
+ // ---------------------------------------------------------------------------
819
+ describe('AuthService.getSettings', () => {
820
+ it('sends GET /settings and returns the public settings', async () => {
821
+ fetchMock.on({
822
+ method: 'GET',
823
+ url: `${PREFIX}/settings`,
824
+ replyCode: 200,
825
+ replyBody: {
826
+ data: {
827
+ disable_signup: false,
828
+ external: { google: true, github: false },
829
+ mailer_autoconfirm: true,
830
+ mfa_enabled: false,
831
+ },
832
+ },
833
+ })
834
+
835
+ const res = await client.auth.getSettings()
836
+
837
+ expect(res.error).toBeNull()
838
+ expect(fetchMock.calls[0].method).toBe('GET')
839
+ expect(fetchMock.calls[0].url).toBe(`${PREFIX}/settings`)
840
+ expect(res.data?.disable_signup).toBe(false)
841
+ expect(res.data?.external).toEqual({ google: true, github: false })
842
+ expect(res.data?.mailer_autoconfirm).toBe(true)
843
+ expect(res.data?.mfa_enabled).toBe(false)
844
+ })
845
+ })
846
+
847
+ // ---------------------------------------------------------------------------
848
+ describe('AuthService.getAnonKey', () => {
849
+ it('sends GET /anon-key and returns the anon key', async () => {
850
+ fetchMock.on({
851
+ method: 'GET',
852
+ url: `${PREFIX}/anon-key`,
853
+ replyCode: 200,
854
+ replyBody: { data: { anon_key: 'anon-key-abc123' } },
855
+ })
856
+
857
+ const res = await client.auth.getAnonKey()
858
+
859
+ expect(res.error).toBeNull()
860
+ expect(fetchMock.calls[0].method).toBe('GET')
861
+ expect(fetchMock.calls[0].url).toBe(`${PREFIX}/anon-key`)
862
+ expect(res.data?.anon_key).toBe('anon-key-abc123')
863
+ })
864
+ })
865
+
866
+ // ---------------------------------------------------------------------------
867
+ describe('AuthService.resetPassword', () => {
868
+ it('sends POST /reset-password with token+password and saves tokens (SIGNED_IN)', async () => {
869
+ const tp = fakeTokenPair()
870
+ fetchMock.on({
871
+ method: 'POST',
872
+ url: `${PREFIX}/reset-password`,
873
+ replyCode: 200,
874
+ replyBody: { data: tp },
875
+ })
876
+
877
+ const events: string[] = []
878
+ client.auth.onAuthStateChange((p) => events.push(p.event))
879
+
880
+ const res = await client.auth.resetPassword('recovery-token-xyz', 'newSecret123')
881
+
882
+ expect(res.error).toBeNull()
883
+ expect(fetchMock.calls[0].method).toBe('POST')
884
+ expect(fetchMock.calls[0].url).toBe(`${PREFIX}/reset-password`)
885
+ expect((fetchMock.calls[0].body as any).token).toBe('recovery-token-xyz')
886
+ expect((fetchMock.calls[0].body as any).password).toBe('newSecret123')
887
+ expect(client.authStore.token).toBe(tp.access_token)
888
+ expect(client.authStore.refreshToken).toBe(tp.refresh_token)
889
+ await Promise.resolve()
890
+ expect(events).toContain('SIGNED_IN')
891
+ })
892
+ })
893
+
894
+ // ---------------------------------------------------------------------------
895
+ describe('AuthService.promoteAnonymous', () => {
896
+ it('sends POST /anonymous/promote with email+password and saves tokens (USER_UPDATED)', async () => {
897
+ const tp = fakeTokenPair()
898
+ fetchMock.on({
899
+ method: 'POST',
900
+ url: `${PREFIX}/anonymous/promote`,
901
+ replyCode: 200,
902
+ replyBody: { data: tp },
903
+ })
904
+
905
+ const events: string[] = []
906
+ client.auth.onAuthStateChange((p) => events.push(p.event))
907
+
908
+ const res = await client.auth.promoteAnonymous({ email: 'promo@example.com', password: 'secret123' })
909
+
910
+ expect(res.error).toBeNull()
911
+ expect(fetchMock.calls[0].method).toBe('POST')
912
+ expect(fetchMock.calls[0].url).toBe(`${PREFIX}/anonymous/promote`)
913
+ expect((fetchMock.calls[0].body as any).email).toBe('promo@example.com')
914
+ expect((fetchMock.calls[0].body as any).password).toBe('secret123')
915
+ expect(client.authStore.token).toBe(tp.access_token)
916
+ expect(client.authStore.refreshToken).toBe(tp.refresh_token)
917
+ await Promise.resolve()
918
+ expect(events).toContain('USER_UPDATED')
919
+ })
920
+ })
921
+
922
+ // ---------------------------------------------------------------------------
923
+ describe('AuthService.verifyMagicLink', () => {
924
+ it('sends POST /magic-link/verify with token and saves tokens (SIGNED_IN)', async () => {
925
+ const tp = fakeTokenPair()
926
+ fetchMock.on({
927
+ method: 'POST',
928
+ url: `${PREFIX}/magic-link/verify`,
929
+ replyCode: 200,
930
+ replyBody: { data: tp },
931
+ })
932
+
933
+ const events: string[] = []
934
+ client.auth.onAuthStateChange((p) => events.push(p.event))
935
+
936
+ const res = await client.auth.verifyMagicLink('magic-token-789')
937
+
938
+ expect(res.error).toBeNull()
939
+ expect(fetchMock.calls[0].method).toBe('POST')
940
+ expect(fetchMock.calls[0].url).toBe(`${PREFIX}/magic-link/verify`)
941
+ expect((fetchMock.calls[0].body as any).token).toBe('magic-token-789')
942
+ expect(client.authStore.token).toBe(tp.access_token)
943
+ expect(client.authStore.refreshToken).toBe(tp.refresh_token)
944
+ await Promise.resolve()
945
+ expect(events).toContain('SIGNED_IN')
946
+ })
947
+ })