@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.
- package/.changeset/config.json +11 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +833 -0
- package/package.json +90 -0
- package/packages/ai/LICENSE +21 -0
- package/packages/ai/README.md +106 -0
- package/packages/ai/dist/index.cjs +229 -0
- package/packages/ai/dist/index.d.cts +139 -0
- package/packages/ai/dist/index.d.ts +139 -0
- package/packages/ai/dist/index.js +226 -0
- package/packages/ai/node_modules/.bin/esbuild +19 -0
- package/packages/ai/node_modules/.bin/tsc +22 -0
- package/packages/ai/node_modules/.bin/tsserver +22 -0
- package/packages/ai/node_modules/.bin/tsup +22 -0
- package/packages/ai/node_modules/.bin/tsup-node +22 -0
- package/packages/ai/node_modules/.bin/vite +22 -0
- package/packages/ai/node_modules/.bin/vitest +22 -0
- package/packages/ai/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/ai/package.json +61 -0
- package/packages/ai/src/AiService.ts +226 -0
- package/packages/ai/src/index.ts +18 -0
- package/packages/ai/src/sse.ts +141 -0
- package/packages/ai/src/types.ts +30 -0
- package/packages/ai/tests/chat-stream.test.ts +151 -0
- package/packages/ai/tests/rag-namespaces.test.ts +54 -0
- package/packages/ai/tests/sse.test.ts +69 -0
- package/packages/ai/tsconfig.json +5 -0
- package/packages/ai/tsup.config.ts +11 -0
- package/packages/aurabase/LICENSE +21 -0
- package/packages/aurabase/README.md +64 -0
- package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.cts +77 -0
- package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.ts +77 -0
- package/packages/aurabase/dist/index.cjs +237 -0
- package/packages/aurabase/dist/index.d.cts +46 -0
- package/packages/aurabase/dist/index.d.ts +46 -0
- package/packages/aurabase/dist/index.js +177 -0
- package/packages/aurabase/dist/ssr.cjs +224 -0
- package/packages/aurabase/dist/ssr.d.cts +65 -0
- package/packages/aurabase/dist/ssr.d.ts +65 -0
- package/packages/aurabase/dist/ssr.js +214 -0
- package/packages/aurabase/node_modules/.bin/esbuild +19 -0
- package/packages/aurabase/node_modules/.bin/tsc +22 -0
- package/packages/aurabase/node_modules/.bin/tsserver +22 -0
- package/packages/aurabase/node_modules/.bin/tsup +22 -0
- package/packages/aurabase/node_modules/.bin/tsup-node +22 -0
- package/packages/aurabase/node_modules/.bin/vite +22 -0
- package/packages/aurabase/node_modules/.bin/vitest +22 -0
- package/packages/aurabase/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/aurabase/package.json +80 -0
- package/packages/aurabase/src/AuraClient.ts +237 -0
- package/packages/aurabase/src/index.ts +152 -0
- package/packages/aurabase/src/ssr.ts +156 -0
- package/packages/aurabase/tests/ai-streaming-rag-e2e.test.ts +82 -0
- package/packages/aurabase/tests/ai.test.ts +111 -0
- package/packages/aurabase/tests/auth-admin.test.ts +515 -0
- package/packages/aurabase/tests/auth-e2e.test.ts +135 -0
- package/packages/aurabase/tests/auth-hardening.test.ts +206 -0
- package/packages/aurabase/tests/auth.test.ts +947 -0
- package/packages/aurabase/tests/database.test.ts +230 -0
- package/packages/aurabase/tests/db-types-e2e.test.ts +217 -0
- package/packages/aurabase/tests/dispose.test.ts +23 -0
- package/packages/aurabase/tests/foreign-token.e2e.test.ts +129 -0
- package/packages/aurabase/tests/functions.test.ts +107 -0
- package/packages/aurabase/tests/integration.test.ts +413 -0
- package/packages/aurabase/tests/live-production-validation.js +241 -0
- package/packages/aurabase/tests/live-sdk-production-validation.js +293 -0
- package/packages/aurabase/tests/mocks.ts +139 -0
- package/packages/aurabase/tests/notifications.test.ts +314 -0
- package/packages/aurabase/tests/observability-backup.test.ts +186 -0
- package/packages/aurabase/tests/observability-hook.test.ts +130 -0
- package/packages/aurabase/tests/production-robustness.test.ts +268 -0
- package/packages/aurabase/tests/public-api-surface.test.ts +53 -0
- package/packages/aurabase/tests/query-builder.test.ts +329 -0
- package/packages/aurabase/tests/readme-smoke-e2e.test.ts +231 -0
- package/packages/aurabase/tests/realtime-filter-e2e.test.ts +344 -0
- package/packages/aurabase/tests/refresh-locks.test.ts +210 -0
- package/packages/aurabase/tests/retry-option.test.ts +17 -0
- package/packages/aurabase/tests/rpc-e2e.test.ts +254 -0
- package/packages/aurabase/tests/rs256-local-e2e.test.ts +114 -0
- package/packages/aurabase/tests/rt-alias-public-e2e.test.ts +235 -0
- package/packages/aurabase/tests/rt-http-broadcast-e2e.test.ts +124 -0
- package/packages/aurabase/tests/service-role-guard.test.ts +99 -0
- package/packages/aurabase/tests/ssr.test.ts +91 -0
- package/packages/aurabase/tests/storage-backend-e2e.test.ts +481 -0
- package/packages/aurabase/tests/storage-sdk-parity-e2e.test.ts +104 -0
- package/packages/aurabase/tests/storage-transform-e2e.test.ts +684 -0
- package/packages/aurabase/tests/storage.test.ts +55 -0
- package/packages/aurabase/tests/types/client-generic.test-d.ts +79 -0
- package/packages/aurabase/tests/types/docs-snippets.test-d.ts +571 -0
- package/packages/aurabase/tests/types/error-code.test-d.ts +53 -0
- package/packages/aurabase/tsconfig.json +5 -0
- package/packages/aurabase/tsconfig.test-d.json +5 -0
- package/packages/aurabase/tsup.config.ts +11 -0
- package/packages/aurabase/vitest.config.ts +16 -0
- package/packages/aurabase/vitest.e2e.config.ts +9 -0
- package/packages/auth/LICENSE +21 -0
- package/packages/auth/README.md +215 -0
- package/packages/auth/dist/index.cjs +1175 -0
- package/packages/auth/dist/index.d.cts +397 -0
- package/packages/auth/dist/index.d.ts +397 -0
- package/packages/auth/dist/index.js +1170 -0
- package/packages/auth/node_modules/.bin/esbuild +19 -0
- package/packages/auth/node_modules/.bin/tsc +22 -0
- package/packages/auth/node_modules/.bin/tsserver +22 -0
- package/packages/auth/node_modules/.bin/tsup +22 -0
- package/packages/auth/node_modules/.bin/tsup-node +22 -0
- package/packages/auth/node_modules/.bin/vite +22 -0
- package/packages/auth/node_modules/.bin/vitest +22 -0
- package/packages/auth/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/auth/package.json +61 -0
- package/packages/auth/src/AuthAdminService.ts +149 -0
- package/packages/auth/src/AuthService.ts +1166 -0
- package/packages/auth/src/SessionManager.ts +92 -0
- package/packages/auth/src/index.ts +17 -0
- package/packages/auth/src/jwks.ts +337 -0
- package/packages/auth/src/pkce.ts +27 -0
- package/packages/auth/src/types.ts +89 -0
- package/packages/auth/tests/auth-lifecycle.test.ts +149 -0
- package/packages/auth/tests/detect-session.test.ts +361 -0
- package/packages/auth/tests/getclaims.test.ts +780 -0
- package/packages/auth/tests/is-mfa-challenge.test.ts +27 -0
- package/packages/auth/tests/password-recovery.test.ts +124 -0
- package/packages/auth/tests/pkce.test.ts +192 -0
- package/packages/auth/tests/session-manager.test.ts +103 -0
- package/packages/auth/tests/set-session.test.ts +139 -0
- package/packages/auth/tests/verify-otp.test.ts +153 -0
- package/packages/auth/tsconfig.json +5 -0
- package/packages/auth/tsup.config.ts +11 -0
- package/packages/core/LICENSE +21 -0
- package/packages/core/README.md +132 -0
- package/packages/core/dist/index.cjs +695 -0
- package/packages/core/dist/index.d.cts +1137 -0
- package/packages/core/dist/index.d.ts +1137 -0
- package/packages/core/dist/index.js +673 -0
- package/packages/core/node_modules/.bin/esbuild +22 -0
- package/packages/core/node_modules/.bin/tsc +22 -0
- package/packages/core/node_modules/.bin/tsserver +22 -0
- package/packages/core/node_modules/.bin/tsup +22 -0
- package/packages/core/node_modules/.bin/tsup-node +22 -0
- package/packages/core/node_modules/.bin/vite +22 -0
- package/packages/core/node_modules/.bin/vitest +22 -0
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/core/package.json +57 -0
- package/packages/core/src/AuraError.ts +89 -0
- package/packages/core/src/BaseService.ts +44 -0
- package/packages/core/src/client.ts +34 -0
- package/packages/core/src/cookie-utils.ts +42 -0
- package/packages/core/src/fetch.ts +435 -0
- package/packages/core/src/index.ts +22 -0
- package/packages/core/src/retry.ts +63 -0
- package/packages/core/src/serialize.ts +6 -0
- package/packages/core/src/stores/BaseAuthStore.ts +164 -0
- package/packages/core/src/stores/LocalAuthStore.ts +199 -0
- package/packages/core/src/types.ts +1042 -0
- package/packages/core/tests/auth-store.test.ts +300 -0
- package/packages/core/tests/error-code.test.ts +61 -0
- package/packages/core/tests/error.test.ts +24 -0
- package/packages/core/tests/fetch.test.ts +286 -0
- package/packages/core/tests/mocks.ts +139 -0
- package/packages/core/tests/observability.test.ts +86 -0
- package/packages/core/tests/refresh-hardening.test.ts +164 -0
- package/packages/core/tests/refresh-observability.test.ts +111 -0
- package/packages/core/tests/retry.test.ts +106 -0
- package/packages/core/tests/serialize.test.ts +12 -0
- package/packages/core/tests/timeout.test.ts +79 -0
- package/packages/core/tsconfig.json +5 -0
- package/packages/core/tsup.config.ts +11 -0
- package/packages/db/LICENSE +21 -0
- package/packages/db/README.md +152 -0
- package/packages/db/VENDORED.md +93 -0
- package/packages/db/dist/index.cjs +544 -0
- package/packages/db/dist/index.d.cts +1111 -0
- package/packages/db/dist/index.d.ts +1111 -0
- package/packages/db/dist/index.js +541 -0
- package/packages/db/node_modules/.bin/esbuild +19 -0
- package/packages/db/node_modules/.bin/tsc +22 -0
- package/packages/db/node_modules/.bin/tsserver +22 -0
- package/packages/db/node_modules/.bin/tsup +22 -0
- package/packages/db/node_modules/.bin/tsup-node +22 -0
- package/packages/db/node_modules/.bin/vite +22 -0
- package/packages/db/node_modules/.bin/vitest +22 -0
- package/packages/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/db/package.json +62 -0
- package/packages/db/src/DatabaseService.ts +170 -0
- package/packages/db/src/QueryBuilder.ts +830 -0
- package/packages/db/src/index.ts +2 -0
- package/packages/db/src/pgrst-types/common/common.ts +86 -0
- package/packages/db/src/pgrst-types/common/rpc.ts +151 -0
- package/packages/db/src/pgrst-types/feature-flags.ts +20 -0
- package/packages/db/src/pgrst-types/types.ts +134 -0
- package/packages/db/src/select-query-parser/parser.ts +479 -0
- package/packages/db/src/select-query-parser/result.ts +553 -0
- package/packages/db/src/select-query-parser/types.ts +132 -0
- package/packages/db/src/select-query-parser/utils.ts +707 -0
- package/packages/db/tests/filters.test.ts +81 -0
- package/packages/db/tests/mutations-filtered.e2e.test.ts +132 -0
- package/packages/db/tests/mutations-filtered.test.ts +188 -0
- package/packages/db/tests/order.test.ts +45 -0
- package/packages/db/tests/rpc.test.ts +41 -0
- package/packages/db/tests/scale/build-large-schema.ts +94 -0
- package/packages/db/tests/scale/scale-compile.test.ts +167 -0
- package/packages/db/tests/schema.test.ts +77 -0
- package/packages/db/tests/single-accept.e2e.test.ts +123 -0
- package/packages/db/tests/single.test.ts +211 -0
- package/packages/db/tests/types/filters.test-d.ts +52 -0
- package/packages/db/tests/types/fixtures.ts +40 -0
- package/packages/db/tests/types/from-views.test-d.ts +18 -0
- package/packages/db/tests/types/mutations.test-d.ts +23 -0
- package/packages/db/tests/types/rpc.test-d.ts +60 -0
- package/packages/db/tests/types/schema-shape.test-d.ts +8 -0
- package/packages/db/tests/types/select-inference.test-d.ts +38 -0
- package/packages/db/tests/vendored/fixtures.ts +83 -0
- package/packages/db/tests/vendored/get-result.test-d.ts +75 -0
- package/packages/db/tsconfig.json +5 -0
- package/packages/db/tsconfig.test-d.json +5 -0
- package/packages/db/tsup.config.ts +11 -0
- package/packages/db/vitest.config.ts +8 -0
- package/packages/functions/LICENSE +21 -0
- package/packages/functions/README.md +96 -0
- package/packages/functions/dist/index.cjs +173 -0
- package/packages/functions/dist/index.d.cts +81 -0
- package/packages/functions/dist/index.d.ts +81 -0
- package/packages/functions/dist/index.js +168 -0
- package/packages/functions/node_modules/.bin/esbuild +19 -0
- package/packages/functions/node_modules/.bin/tsc +22 -0
- package/packages/functions/node_modules/.bin/tsserver +22 -0
- package/packages/functions/node_modules/.bin/tsup +22 -0
- package/packages/functions/node_modules/.bin/tsup-node +22 -0
- package/packages/functions/node_modules/.bin/vite +22 -0
- package/packages/functions/node_modules/.bin/vitest +22 -0
- package/packages/functions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/functions/package.json +60 -0
- package/packages/functions/src/FunctionsService.ts +188 -0
- package/packages/functions/src/errors.ts +70 -0
- package/packages/functions/src/index.ts +6 -0
- package/packages/functions/tests/invoke.test.ts +97 -0
- package/packages/functions/tests/response-type-errors.test.ts +135 -0
- package/packages/functions/tsconfig.json +5 -0
- package/packages/functions/tsup.config.ts +11 -0
- package/packages/gen/LICENSE +21 -0
- package/packages/gen/README.md +84 -0
- package/packages/gen/dist/cli.cjs +334 -0
- package/packages/gen/dist/cli.d.cts +1 -0
- package/packages/gen/dist/cli.d.ts +1 -0
- package/packages/gen/dist/cli.js +332 -0
- package/packages/gen/dist/index.cjs +257 -0
- package/packages/gen/dist/index.d.cts +12 -0
- package/packages/gen/dist/index.d.ts +12 -0
- package/packages/gen/dist/index.js +255 -0
- package/packages/gen/node_modules/.bin/esbuild +19 -0
- package/packages/gen/node_modules/.bin/tsc +22 -0
- package/packages/gen/node_modules/.bin/tsserver +22 -0
- package/packages/gen/node_modules/.bin/tsup +22 -0
- package/packages/gen/node_modules/.bin/tsup-node +22 -0
- package/packages/gen/node_modules/.bin/vite +22 -0
- package/packages/gen/node_modules/.bin/vitest +22 -0
- package/packages/gen/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/gen/package.json +66 -0
- package/packages/gen/src/cli.ts +67 -0
- package/packages/gen/src/generate.ts +0 -0
- package/packages/gen/src/index.ts +2 -0
- package/packages/gen/tests/compile.test.ts +213 -0
- package/packages/gen/tests/e2e.test.ts +130 -0
- package/packages/gen/tests/generate.test.ts +396 -0
- package/packages/gen/tsconfig.json +5 -0
- package/packages/gen/tsup.config.ts +11 -0
- package/packages/notifications/LICENSE +21 -0
- package/packages/notifications/README.md +134 -0
- package/packages/notifications/dist/index.cjs +128 -0
- package/packages/notifications/dist/index.d.cts +81 -0
- package/packages/notifications/dist/index.d.ts +81 -0
- package/packages/notifications/dist/index.js +125 -0
- package/packages/notifications/node_modules/.bin/esbuild +19 -0
- package/packages/notifications/node_modules/.bin/tsc +22 -0
- package/packages/notifications/node_modules/.bin/tsserver +22 -0
- package/packages/notifications/node_modules/.bin/tsup +22 -0
- package/packages/notifications/node_modules/.bin/tsup-node +22 -0
- package/packages/notifications/node_modules/.bin/vite +22 -0
- package/packages/notifications/node_modules/.bin/vitest +22 -0
- package/packages/notifications/package.json +60 -0
- package/packages/notifications/src/NotificationsService.ts +230 -0
- package/packages/notifications/src/index.ts +19 -0
- package/packages/notifications/tsconfig.json +5 -0
- package/packages/notifications/tsup.config.ts +11 -0
- package/packages/realtime/LICENSE +21 -0
- package/packages/realtime/README.md +183 -0
- package/packages/realtime/dist/index.cjs +977 -0
- package/packages/realtime/dist/index.d.cts +259 -0
- package/packages/realtime/dist/index.d.ts +259 -0
- package/packages/realtime/dist/index.js +973 -0
- package/packages/realtime/node_modules/.bin/esbuild +19 -0
- package/packages/realtime/node_modules/.bin/tsc +22 -0
- package/packages/realtime/node_modules/.bin/tsserver +22 -0
- package/packages/realtime/node_modules/.bin/tsup +22 -0
- package/packages/realtime/node_modules/.bin/tsup-node +22 -0
- package/packages/realtime/node_modules/.bin/vite +22 -0
- package/packages/realtime/node_modules/.bin/vitest +22 -0
- package/packages/realtime/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/realtime/package.json +60 -0
- package/packages/realtime/src/AuraChannel.ts +485 -0
- package/packages/realtime/src/HeartbeatManager.ts +73 -0
- package/packages/realtime/src/RealtimeService.ts +505 -0
- package/packages/realtime/src/columnFilter.ts +301 -0
- package/packages/realtime/src/index.ts +4 -0
- package/packages/realtime/tests/ack-routing.test.ts +81 -0
- package/packages/realtime/tests/column-filter.test.ts +144 -0
- package/packages/realtime/tests/dispatch.test.ts +191 -0
- package/packages/realtime/tests/heartbeat.test.ts +67 -0
- package/packages/realtime/tests/http-broadcast.test.ts +68 -0
- package/packages/realtime/tests/realtime-token.test.ts +43 -0
- package/packages/realtime/tests/reconnect-race.test.ts +149 -0
- package/packages/realtime/tests/ref-id.test.ts +34 -0
- package/packages/realtime/tests/set-auth.test.ts +70 -0
- package/packages/realtime/tsconfig.json +5 -0
- package/packages/realtime/tsup.config.ts +11 -0
- package/packages/storage/LICENSE +21 -0
- package/packages/storage/README.md +141 -0
- package/packages/storage/dist/index.cjs +333 -0
- package/packages/storage/dist/index.d.cts +185 -0
- package/packages/storage/dist/index.d.ts +185 -0
- package/packages/storage/dist/index.js +329 -0
- package/packages/storage/node_modules/.bin/esbuild +19 -0
- package/packages/storage/node_modules/.bin/tsc +22 -0
- package/packages/storage/node_modules/.bin/tsserver +22 -0
- package/packages/storage/node_modules/.bin/tsup +22 -0
- package/packages/storage/node_modules/.bin/tsup-node +22 -0
- package/packages/storage/node_modules/.bin/vite +22 -0
- package/packages/storage/node_modules/.bin/vitest +22 -0
- package/packages/storage/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/storage/package.json +60 -0
- package/packages/storage/src/DownloadBuilder.ts +60 -0
- package/packages/storage/src/StorageService.ts +323 -0
- package/packages/storage/src/index.ts +2 -0
- package/packages/storage/src/path.ts +35 -0
- package/packages/storage/src/transform.ts +15 -0
- package/packages/storage/tests/download.test.ts +64 -0
- package/packages/storage/tests/parity.test.ts +154 -0
- package/packages/storage/tests/path.test.ts +87 -0
- package/packages/storage/tests/transform.test.ts +79 -0
- package/packages/storage/tsconfig.json +5 -0
- package/packages/storage/tsup.config.ts +11 -0
- package/pnpm-workspace.yaml +24 -0
- package/tsconfig.base.json +15 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// FetchMock -- intercepts globalThis.fetch for deterministic SDK tests
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export interface RequestMock {
|
|
6
|
+
method: string
|
|
7
|
+
url: string | RegExp
|
|
8
|
+
replyCode: number
|
|
9
|
+
replyBody?: unknown
|
|
10
|
+
replyHeaders?: Record<string, string>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FetchCall {
|
|
14
|
+
url: string
|
|
15
|
+
method: string
|
|
16
|
+
headers: Headers
|
|
17
|
+
body?: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class FetchMock {
|
|
21
|
+
private originalFetch?: typeof fetch
|
|
22
|
+
private mocks: RequestMock[] = []
|
|
23
|
+
calls: FetchCall[] = []
|
|
24
|
+
|
|
25
|
+
/** Register a mock response. */
|
|
26
|
+
on(mock: RequestMock): void {
|
|
27
|
+
this.mocks.push(mock)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Replace globalThis.fetch with the mock implementation. */
|
|
31
|
+
init(): void {
|
|
32
|
+
this.originalFetch = globalThis.fetch
|
|
33
|
+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
34
|
+
const url = typeof input === 'string' ? input : input.toString()
|
|
35
|
+
const method = (init?.method ?? 'GET').toUpperCase()
|
|
36
|
+
const headers = new Headers(init?.headers)
|
|
37
|
+
let body: unknown
|
|
38
|
+
if (typeof init?.body === 'string') {
|
|
39
|
+
try {
|
|
40
|
+
body = JSON.parse(init.body)
|
|
41
|
+
} catch {
|
|
42
|
+
body = init.body
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
body = init?.body
|
|
46
|
+
}
|
|
47
|
+
this.calls.push({ url, method, headers, body })
|
|
48
|
+
|
|
49
|
+
for (const mock of this.mocks) {
|
|
50
|
+
const urlMatch =
|
|
51
|
+
mock.url instanceof RegExp ? mock.url.test(url) : mock.url === url
|
|
52
|
+
if (urlMatch && mock.method.toUpperCase() === method) {
|
|
53
|
+
return new Response(
|
|
54
|
+
mock.replyBody !== undefined ? JSON.stringify(mock.replyBody) : null,
|
|
55
|
+
{
|
|
56
|
+
status: mock.replyCode,
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
...mock.replyHeaders,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new Error(`FetchMock: no mock for ${method} ${url}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Restore the original globalThis.fetch. */
|
|
71
|
+
restore(): void {
|
|
72
|
+
if (this.originalFetch) {
|
|
73
|
+
globalThis.fetch = this.originalFetch
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Remove all registered mocks. */
|
|
78
|
+
clearMocks(): void {
|
|
79
|
+
this.mocks = []
|
|
80
|
+
this.calls = []
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Helpers -- reusable test data factories
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
import type { User, TokenPair } from '../src/types'
|
|
89
|
+
|
|
90
|
+
/** Build a fake JWT whose payload encodes the given exp (seconds since epoch). */
|
|
91
|
+
export function fakeJwt(exp: number): string {
|
|
92
|
+
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
|
|
93
|
+
.replace(/\+/g, '-')
|
|
94
|
+
.replace(/\//g, '_')
|
|
95
|
+
.replace(/=+$/, '')
|
|
96
|
+
const payload = btoa(JSON.stringify({ sub: 'user-1', exp }))
|
|
97
|
+
.replace(/\+/g, '-')
|
|
98
|
+
.replace(/\//g, '_')
|
|
99
|
+
.replace(/=+$/, '')
|
|
100
|
+
const sig = 'fake_sig'
|
|
101
|
+
return `${header}.${payload}.${sig}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A JWT that expires far in the future. */
|
|
105
|
+
export function validJwt(): string {
|
|
106
|
+
return fakeJwt(Math.floor(Date.now() / 1000) + 3600)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A JWT that is already expired. */
|
|
110
|
+
export function expiredJwt(): string {
|
|
111
|
+
return fakeJwt(Math.floor(Date.now() / 1000) - 60)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function fakeUser(overrides: Partial<User> = {}): User {
|
|
115
|
+
return {
|
|
116
|
+
id: 'user-1',
|
|
117
|
+
email: 'test@example.com',
|
|
118
|
+
role: 'authenticated',
|
|
119
|
+
email_verified: true,
|
|
120
|
+
is_banned: false,
|
|
121
|
+
is_anonymous: false,
|
|
122
|
+
mfa_enabled: false,
|
|
123
|
+
user_metadata: {},
|
|
124
|
+
app_metadata: {},
|
|
125
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
126
|
+
...overrides,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function fakeTokenPair(overrides: Partial<TokenPair> = {}): TokenPair {
|
|
131
|
+
return {
|
|
132
|
+
access_token: validJwt(),
|
|
133
|
+
refresh_token: 'rt_fake_refresh',
|
|
134
|
+
token_type: 'bearer',
|
|
135
|
+
expires_in: 3600,
|
|
136
|
+
user: fakeUser(),
|
|
137
|
+
...overrides,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// emitAuraEvent (LOT 3, finding I8) -- observability hook helper
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
6
|
+
import { emitAuraEvent, type AuraObservabilityEvent } from '../src/types'
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('emitAuraEvent', () => {
|
|
13
|
+
it('appelle onEvent avec l’événement tel quel', () => {
|
|
14
|
+
const received: AuraObservabilityEvent[] = []
|
|
15
|
+
const event: AuraObservabilityEvent = { kind: 'timeout', url: 'http://x/y' }
|
|
16
|
+
emitAuraEvent((e) => received.push(e), false, event)
|
|
17
|
+
expect(received).toEqual([event])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('ne fait rien si onEvent est undefined (pas de throw)', () => {
|
|
21
|
+
expect(() => emitAuraEvent(undefined, false, { kind: 'refresh-success' })).not.toThrow()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('NE THROW JAMAIS si le hook onEvent lève une exception', () => {
|
|
25
|
+
const boom = () => {
|
|
26
|
+
throw new Error('consumer hook boom')
|
|
27
|
+
}
|
|
28
|
+
expect(() =>
|
|
29
|
+
emitAuraEvent(boom, false, { kind: 'refresh-discarded', reason: 'test' }),
|
|
30
|
+
).not.toThrow()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('debug:true journalise via console.debug en plus du hook', () => {
|
|
34
|
+
const spy = vi.spyOn(console, 'debug').mockImplementation(() => {})
|
|
35
|
+
const received: AuraObservabilityEvent[] = []
|
|
36
|
+
const event: AuraObservabilityEvent = { kind: 'jwks-fetch-failed', baseUrl: 'http://x' }
|
|
37
|
+
emitAuraEvent((e) => received.push(e), true, event)
|
|
38
|
+
expect(spy).toHaveBeenCalledWith('[aurabase]', event)
|
|
39
|
+
expect(received).toEqual([event])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('debug:true SANS onEvent journalise quand même via console.debug', () => {
|
|
43
|
+
const spy = vi.spyOn(console, 'debug').mockImplementation(() => {})
|
|
44
|
+
emitAuraEvent(undefined, true, { kind: 'storage-blocked', op: 'setItem' })
|
|
45
|
+
expect(spy).toHaveBeenCalledTimes(1)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('debug:false (ou omis) ne journalise pas', () => {
|
|
49
|
+
const spy = vi.spyOn(console, 'debug').mockImplementation(() => {})
|
|
50
|
+
emitAuraEvent(() => {}, false, { kind: 'refresh-success' })
|
|
51
|
+
expect(spy).not.toHaveBeenCalled()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('minor 6a : RÉDIGE la query/fragment de `url` (retry/timeout) — pas de fuite de capacité bearer', () => {
|
|
55
|
+
const received: AuraObservabilityEvent[] = []
|
|
56
|
+
emitAuraEvent((e) => received.push(e), false, {
|
|
57
|
+
kind: 'retry',
|
|
58
|
+
attempt: 1,
|
|
59
|
+
url: 'https://api.host/v1/db/proj/rest/posts?apikey=aura_sk_secret&token=abc#frag',
|
|
60
|
+
})
|
|
61
|
+
const got = received[0] as { kind: string; url: string }
|
|
62
|
+
expect(got.url).toBe('https://api.host/v1/db/proj/rest/posts')
|
|
63
|
+
expect(got.url).not.toContain('apikey')
|
|
64
|
+
expect(got.url).not.toContain('token')
|
|
65
|
+
expect(got.url).not.toContain('#')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('minor 6a : `url` sans query est inchangée ; `baseUrl` (jwks) jamais touché', () => {
|
|
69
|
+
const received: AuraObservabilityEvent[] = []
|
|
70
|
+
emitAuraEvent((e) => received.push(e), false, { kind: 'timeout', url: 'http://x/y' })
|
|
71
|
+
emitAuraEvent((e) => received.push(e), false, { kind: 'jwks-fetch-failed', baseUrl: 'http://x' })
|
|
72
|
+
expect((received[0] as { url: string }).url).toBe('http://x/y')
|
|
73
|
+
expect((received[1] as { baseUrl: string }).baseUrl).toBe('http://x')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('une exception dans console.debug ne casse pas non plus l’émission', () => {
|
|
77
|
+
vi.spyOn(console, 'debug').mockImplementation(() => {
|
|
78
|
+
throw new Error('console boom')
|
|
79
|
+
})
|
|
80
|
+
const received: AuraObservabilityEvent[] = []
|
|
81
|
+
expect(() =>
|
|
82
|
+
emitAuraEvent((e) => received.push(e), true, { kind: 'refresh-success' }),
|
|
83
|
+
).not.toThrow()
|
|
84
|
+
expect(received).toHaveLength(1)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import { refreshOnce } from '../src/fetch'
|
|
3
|
+
import { BaseAuthStore } from '../src/stores/BaseAuthStore'
|
|
4
|
+
import type { AuraClientLike } from '../src/client'
|
|
5
|
+
import { fakeTokenPair, fakeUser, validJwt } from './mocks'
|
|
6
|
+
|
|
7
|
+
afterEach(() => vi.useRealTimers())
|
|
8
|
+
|
|
9
|
+
function client(fetchFn: typeof globalThis.fetch, store = new BaseAuthStore()): AuraClientLike {
|
|
10
|
+
store.save(validJwt(), 'rt_current', fakeUser())
|
|
11
|
+
return {
|
|
12
|
+
baseUrl: 'http://x',
|
|
13
|
+
projectId: 'p',
|
|
14
|
+
apiKey: 'k',
|
|
15
|
+
fetchFn,
|
|
16
|
+
authStore: store,
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
auth: { refreshSession: async () => ({ data: null, error: null }) as any },
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('BaseAuthStore.removalEpoch', () => {
|
|
23
|
+
it('incrémente à chaque clear()', () => {
|
|
24
|
+
const s = new BaseAuthStore()
|
|
25
|
+
expect(s.removalEpoch).toBe(0)
|
|
26
|
+
s.clear()
|
|
27
|
+
expect(s.removalEpoch).toBe(1)
|
|
28
|
+
s.clear()
|
|
29
|
+
expect(s.removalEpoch).toBe(2)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('commit-guard : refresh jeté si clear() concurrent', () => {
|
|
34
|
+
it('ne sauvegarde pas les tokens si la session a été vidée pendant le refresh', async () => {
|
|
35
|
+
const store = new BaseAuthStore()
|
|
36
|
+
let resolveFetch: (r: Response) => void
|
|
37
|
+
const fetchFn = vi.fn().mockImplementation(
|
|
38
|
+
() => new Promise<Response>((res) => { resolveFetch = res }),
|
|
39
|
+
)
|
|
40
|
+
const c = client(fetchFn as unknown as typeof globalThis.fetch, store)
|
|
41
|
+
const p = refreshOnce(c)
|
|
42
|
+
store.clear() // un signOut arrive pendant l'await du fetch
|
|
43
|
+
resolveFetch!(
|
|
44
|
+
new Response(JSON.stringify(fakeTokenPair({ refresh_token: 'rt_new' })), {
|
|
45
|
+
status: 200,
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
const res = await p
|
|
50
|
+
expect(res.error?.status).toBe(409)
|
|
51
|
+
expect(res.error?.code).toBe('refresh_discarded')
|
|
52
|
+
expect(store.token).toBeNull() // pas ressuscité
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('proactive-preserve + commit-guard (sans cooldown)', () => {
|
|
57
|
+
it('échec transitoire (5xx) préserve la session ET ne cache PAS (2e refresh re-tape le réseau)', async () => {
|
|
58
|
+
const store = new BaseAuthStore()
|
|
59
|
+
store.save(validJwt(), 'rt_stable', fakeUser())
|
|
60
|
+
const fetchFn = vi.fn().mockResolvedValue(
|
|
61
|
+
new Response(JSON.stringify({ error: { code: 'server_error', message: 'boom' } }), {
|
|
62
|
+
status: 500,
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
const c = {
|
|
67
|
+
baseUrl: 'http://x', projectId: 'p', apiKey: 'k',
|
|
68
|
+
fetchFn: fetchFn as unknown as typeof globalThis.fetch, authStore: store,
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
auth: { refreshSession: async () => ({ data: null, error: null }) as any },
|
|
71
|
+
} as AuraClientLike
|
|
72
|
+
const r1 = await refreshOnce(c)
|
|
73
|
+
expect(r1.error).toBeTruthy()
|
|
74
|
+
expect(store.token).not.toBeNull() // 5xx transitoire → session PRÉSERVÉE
|
|
75
|
+
const r2 = await refreshOnce(c)
|
|
76
|
+
expect(r2.error).toBeTruthy()
|
|
77
|
+
expect(fetchFn).toHaveBeenCalledTimes(2) // PAS de cooldown → 2e re-tape le réseau
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('FIX1 succès : un signIn concurrent (rt différent) n’est PAS écrasé par le refresh de l’ancien token', async () => {
|
|
81
|
+
const store = new BaseAuthStore()
|
|
82
|
+
store.save(validJwt(), 'rt_old', fakeUser())
|
|
83
|
+
let resolveFetch!: (r: Response) => void
|
|
84
|
+
const fetchFn = vi.fn().mockImplementation(() => new Promise<Response>((res) => { resolveFetch = res }))
|
|
85
|
+
const c = {
|
|
86
|
+
baseUrl: 'http://x', projectId: 'p', apiKey: 'k',
|
|
87
|
+
fetchFn: fetchFn as unknown as typeof globalThis.fetch, authStore: store,
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
auth: { refreshSession: async () => ({ data: null, error: null }) as any },
|
|
90
|
+
} as AuraClientLike
|
|
91
|
+
const p = refreshOnce(c)
|
|
92
|
+
store.save(validJwt(), 'rt_new', fakeUser()) // signIn concurrent
|
|
93
|
+
resolveFetch(new Response(JSON.stringify(fakeTokenPair({ refresh_token: 'rt_rotated' })), {
|
|
94
|
+
status: 200, headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
}))
|
|
96
|
+
const r = await p
|
|
97
|
+
expect(r.error?.code).toBe('refresh_discarded')
|
|
98
|
+
expect(store.refreshToken).toBe('rt_new') // session neuve PRÉSERVÉE
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('FIX1 échec : un signIn concurrent n’est PAS effacé par un 401 sur l’ancien token', async () => {
|
|
102
|
+
const store = new BaseAuthStore()
|
|
103
|
+
store.save(validJwt(), 'rt_old', fakeUser())
|
|
104
|
+
let resolveFetch!: (r: Response) => void
|
|
105
|
+
const fetchFn = vi.fn().mockImplementation(() => new Promise<Response>((res) => { resolveFetch = res }))
|
|
106
|
+
const c = {
|
|
107
|
+
baseUrl: 'http://x', projectId: 'p', apiKey: 'k',
|
|
108
|
+
fetchFn: fetchFn as unknown as typeof globalThis.fetch, authStore: store,
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
|
+
auth: { refreshSession: async () => ({ data: null, error: null }) as any },
|
|
111
|
+
} as AuraClientLike
|
|
112
|
+
const p = refreshOnce(c)
|
|
113
|
+
store.save(validJwt(), 'rt_new', fakeUser()) // signIn concurrent
|
|
114
|
+
resolveFetch(new Response(JSON.stringify({ error: { code: 'invalid_grant', message: 'revoked' } }), {
|
|
115
|
+
status: 401, headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
}))
|
|
117
|
+
await p
|
|
118
|
+
expect(store.token).not.toBeNull() // NON effacée
|
|
119
|
+
expect(store.refreshToken).toBe('rt_new') // session neuve intacte
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('rejet définitif (401) → session détruite', async () => {
|
|
123
|
+
const store = new BaseAuthStore()
|
|
124
|
+
store.save(validJwt(), 'rt_x', fakeUser())
|
|
125
|
+
const fetchFn = vi.fn().mockResolvedValue(
|
|
126
|
+
new Response(JSON.stringify({ error: { code: 'invalid_grant', message: 'revoked' } }), {
|
|
127
|
+
status: 401,
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
const c = {
|
|
132
|
+
baseUrl: 'http://x',
|
|
133
|
+
projectId: 'p',
|
|
134
|
+
apiKey: 'k',
|
|
135
|
+
fetchFn: fetchFn as unknown as typeof globalThis.fetch,
|
|
136
|
+
authStore: store,
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
138
|
+
auth: { refreshSession: async () => ({ data: null, error: null }) as any },
|
|
139
|
+
} as AuraClientLike
|
|
140
|
+
const r = await refreshOnce(c)
|
|
141
|
+
expect(r.error?.status).toBe(401)
|
|
142
|
+
expect(store.token).toBeNull() // session morte
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('FIX4 : corps de succès non-JSON', () => {
|
|
147
|
+
it('un 200 au corps non-JSON renvoie invalid_response (pas de throw) et préserve la session', async () => {
|
|
148
|
+
const store = new BaseAuthStore()
|
|
149
|
+
const fetchFn = vi.fn().mockResolvedValue(
|
|
150
|
+
// 200 OK mais corps HTML (proxy mal configuré) → res.json() throw.
|
|
151
|
+
new Response('<html>Service Unavailable</html>', {
|
|
152
|
+
status: 200,
|
|
153
|
+
headers: { 'Content-Type': 'text/html' },
|
|
154
|
+
}),
|
|
155
|
+
)
|
|
156
|
+
const c = client(fetchFn as unknown as typeof globalThis.fetch, store)
|
|
157
|
+
// Ne doit PAS rejeter : refreshOnce renvoie toujours un AuraResponse.
|
|
158
|
+
const r = await refreshOnce(c)
|
|
159
|
+
expect(r.data).toBeNull()
|
|
160
|
+
expect(r.error?.code).toBe('invalid_response')
|
|
161
|
+
// Un succès illisible n'est pas un rejet définitif → session préservée.
|
|
162
|
+
expect(store.token).not.toBeNull()
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Observabilité du refresh (LOT 3, finding I8) : refresh-success / refresh-discarded
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// NOTE : n'utilise PAS le pattern `resolveFetch` manuel de `refresh-hardening.test.ts`
|
|
6
|
+
// (pré-existant, flaky indépendamment de ce lot — cf. Web Locks LOT 2). On s'appuie
|
|
7
|
+
// à la place sur `mockResolvedValue` (le fetch est déjà résolu, mais `await` cède
|
|
8
|
+
// quand même la main le temps d'un microtask), ce qui suffit à ouvrir la fenêtre de
|
|
9
|
+
// course nécessaire au test `refresh-discarded` sans dépendre d'un timing fragile.
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, afterEach, vi } from 'vitest'
|
|
12
|
+
import { refreshOnce } from '../src/fetch'
|
|
13
|
+
import { BaseAuthStore } from '../src/stores/BaseAuthStore'
|
|
14
|
+
import type { AuraClientLike } from '../src/client'
|
|
15
|
+
import { fakeTokenPair, fakeUser, validJwt } from './mocks'
|
|
16
|
+
import type { AuraObservabilityEvent } from '../src/types'
|
|
17
|
+
|
|
18
|
+
afterEach(() => vi.useRealTimers())
|
|
19
|
+
|
|
20
|
+
function client(
|
|
21
|
+
fetchFn: typeof globalThis.fetch,
|
|
22
|
+
onEvent: (e: AuraObservabilityEvent) => void,
|
|
23
|
+
store = new BaseAuthStore(),
|
|
24
|
+
): AuraClientLike {
|
|
25
|
+
store.save(validJwt(), 'rt_current', fakeUser())
|
|
26
|
+
return {
|
|
27
|
+
baseUrl: 'http://x',
|
|
28
|
+
projectId: 'p',
|
|
29
|
+
apiKey: 'k',
|
|
30
|
+
fetchFn,
|
|
31
|
+
authStore: store,
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
auth: { refreshSession: async () => ({ data: null, error: null }) as any },
|
|
34
|
+
onEvent,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('refreshOnce — observabilité (onEvent)', () => {
|
|
39
|
+
it('émet {kind:"refresh-success"} quand le refresh aboutit', async () => {
|
|
40
|
+
const events: AuraObservabilityEvent[] = []
|
|
41
|
+
const fetchFn = vi.fn().mockResolvedValue(
|
|
42
|
+
new Response(JSON.stringify(fakeTokenPair({ refresh_token: 'rt_new' })), {
|
|
43
|
+
status: 200,
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
const store = new BaseAuthStore()
|
|
48
|
+
const c = client(fetchFn as unknown as typeof globalThis.fetch, (e) => events.push(e), store)
|
|
49
|
+
// rt_current doit correspondre au refresh_token utilisé par doRefresh (posé par client()).
|
|
50
|
+
const res = await refreshOnce(c)
|
|
51
|
+
expect(res.error).toBeNull()
|
|
52
|
+
expect(events).toContainEqual({ kind: 'refresh-success' })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('émet {kind:"refresh-discarded", reason} quand la session change pendant le refresh (commit-guard)', async () => {
|
|
56
|
+
const events: AuraObservabilityEvent[] = []
|
|
57
|
+
const store = new BaseAuthStore()
|
|
58
|
+
let resolveFetch!: (r: Response) => void
|
|
59
|
+
const fetchFn = vi.fn().mockImplementation(
|
|
60
|
+
() => new Promise<Response>((res) => { resolveFetch = res }),
|
|
61
|
+
)
|
|
62
|
+
const c = client(fetchFn as unknown as typeof globalThis.fetch, (e) => events.push(e), store)
|
|
63
|
+
const p = refreshOnce(c)
|
|
64
|
+
// NOTE (à jour) : `withRefreshLock` est GATÉ sur `window` — hors navigateur (ces
|
|
65
|
+
// tests s'exécutent sous Node SANS `window`), `navigator.locks` n'est PAS utilisé
|
|
66
|
+
// même s'il existe (Node ≥21) ; `doRefresh` s'exécute donc directement (single-flight
|
|
67
|
+
// in-process seul). On garde néanmoins `waitFor` (robuste au timing des microtasks)
|
|
68
|
+
// pour laisser `doRefresh` atteindre `await client.fetchFn(...)` (donc `resolveFetch`
|
|
69
|
+
// assigné) avant d'intercaler le `clear()` concurrent.
|
|
70
|
+
await vi.waitFor(() => expect(fetchFn).toHaveBeenCalled(), { timeout: 2000, interval: 5 })
|
|
71
|
+
store.clear() // un signOut concurrent s'intercale avant la résolution du fetch
|
|
72
|
+
resolveFetch(
|
|
73
|
+
new Response(JSON.stringify(fakeTokenPair({ refresh_token: 'rt_new_from_server' })), {
|
|
74
|
+
status: 200,
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
}),
|
|
77
|
+
)
|
|
78
|
+
const res = await p
|
|
79
|
+
expect(res.error?.code).toBe('refresh_discarded')
|
|
80
|
+
const discarded = events.find((e) => e.kind === 'refresh-discarded')
|
|
81
|
+
expect(discarded).toBeDefined()
|
|
82
|
+
expect((discarded as { kind: 'refresh-discarded'; reason: string }).reason).toEqual(
|
|
83
|
+
expect.any(String),
|
|
84
|
+
)
|
|
85
|
+
// Pas de refresh-success émis dans ce scénario.
|
|
86
|
+
expect(events.some((e) => e.kind === 'refresh-success')).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('onEvent absent (undefined) : aucun throw, comportement inchangé', async () => {
|
|
90
|
+
const fetchFn = vi.fn().mockResolvedValue(
|
|
91
|
+
new Response(JSON.stringify(fakeTokenPair({ refresh_token: 'rt_new' })), {
|
|
92
|
+
status: 200,
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
96
|
+
const store = new BaseAuthStore()
|
|
97
|
+
store.save(validJwt(), 'rt_current', fakeUser())
|
|
98
|
+
const c: AuraClientLike = {
|
|
99
|
+
baseUrl: 'http://x',
|
|
100
|
+
projectId: 'p',
|
|
101
|
+
apiKey: 'k',
|
|
102
|
+
fetchFn: fetchFn as unknown as typeof globalThis.fetch,
|
|
103
|
+
authStore: store,
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
auth: { refreshSession: async () => ({ data: null, error: null }) as any },
|
|
106
|
+
// onEvent volontairement omis
|
|
107
|
+
}
|
|
108
|
+
const res = await refreshOnce(c)
|
|
109
|
+
expect(res.error).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
isRetryableStatus,
|
|
4
|
+
isRetryableMethod,
|
|
5
|
+
computeBackoff,
|
|
6
|
+
parseRetryAfter,
|
|
7
|
+
abortableSleep,
|
|
8
|
+
isAbortError,
|
|
9
|
+
DEFAULT_MAX_RETRIES,
|
|
10
|
+
} from '../src/retry'
|
|
11
|
+
|
|
12
|
+
describe('retry — prédicats', () => {
|
|
13
|
+
it('isRetryableStatus', () => {
|
|
14
|
+
for (const s of [408, 429, 502, 503, 504]) expect(isRetryableStatus(s)).toBe(true)
|
|
15
|
+
for (const s of [200, 400, 401, 404, 500, 501]) expect(isRetryableStatus(s)).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
it('isRetryableMethod (insensible à la casse)', () => {
|
|
18
|
+
for (const m of ['GET', 'get', 'HEAD', 'head']) expect(isRetryableMethod(m)).toBe(true)
|
|
19
|
+
for (const m of ['POST', 'PATCH', 'DELETE', 'PUT']) expect(isRetryableMethod(m)).toBe(false)
|
|
20
|
+
})
|
|
21
|
+
it('DEFAULT_MAX_RETRIES vaut 3', () => {
|
|
22
|
+
expect(DEFAULT_MAX_RETRIES).toBe(3)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('computeBackoff — full jitter', () => {
|
|
27
|
+
it('borne = min(500·2^attempt, 10000)', () => {
|
|
28
|
+
expect(computeBackoff(0, () => 1)).toBe(500)
|
|
29
|
+
expect(computeBackoff(1, () => 1)).toBe(1000)
|
|
30
|
+
expect(computeBackoff(2, () => 1)).toBe(2000)
|
|
31
|
+
expect(computeBackoff(10, () => 1)).toBe(10000) // plafonné
|
|
32
|
+
})
|
|
33
|
+
it('rand=0 → 0', () => {
|
|
34
|
+
expect(computeBackoff(3, () => 0)).toBe(0)
|
|
35
|
+
})
|
|
36
|
+
it('valeur ∈ [0, borne]', () => {
|
|
37
|
+
const v = computeBackoff(2, () => 0.5)
|
|
38
|
+
expect(v).toBeGreaterThanOrEqual(0)
|
|
39
|
+
expect(v).toBeLessThanOrEqual(2000)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('parseRetryAfter', () => {
|
|
44
|
+
it('secondes → ms', () => {
|
|
45
|
+
expect(parseRetryAfter('5')).toBe(5000)
|
|
46
|
+
expect(parseRetryAfter('0')).toBe(0)
|
|
47
|
+
})
|
|
48
|
+
it('HTTP-date → delta ms (now injecté)', () => {
|
|
49
|
+
const now = 1_000_000_000_000
|
|
50
|
+
const future = new Date(now + 3000).toUTCString()
|
|
51
|
+
expect(parseRetryAfter(future, now)).toBe(3000)
|
|
52
|
+
})
|
|
53
|
+
it('date passée → 0 (jamais négatif)', () => {
|
|
54
|
+
const now = 1_000_000_000_000
|
|
55
|
+
const past = new Date(now - 5000).toUTCString()
|
|
56
|
+
expect(parseRetryAfter(past, now)).toBe(0)
|
|
57
|
+
})
|
|
58
|
+
it('null ou garbage → null', () => {
|
|
59
|
+
expect(parseRetryAfter(null)).toBeNull()
|
|
60
|
+
expect(parseRetryAfter('not-a-date')).toBeNull()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('abortableSleep', () => {
|
|
65
|
+
afterEach(() => vi.useRealTimers())
|
|
66
|
+
it('résout après le délai', async () => {
|
|
67
|
+
vi.useFakeTimers()
|
|
68
|
+
let done = false
|
|
69
|
+
const p = abortableSleep(1000).then(() => {
|
|
70
|
+
done = true
|
|
71
|
+
})
|
|
72
|
+
await vi.advanceTimersByTimeAsync(999)
|
|
73
|
+
expect(done).toBe(false)
|
|
74
|
+
await vi.advanceTimersByTimeAsync(1)
|
|
75
|
+
await p
|
|
76
|
+
expect(done).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
it('signal déjà aborté → résout immédiatement', async () => {
|
|
79
|
+
const ac = new AbortController()
|
|
80
|
+
ac.abort()
|
|
81
|
+
await expect(abortableSleep(100000, ac.signal)).resolves.toBeUndefined()
|
|
82
|
+
})
|
|
83
|
+
it('abort pendant l’attente → résout tôt (sans rejeter)', async () => {
|
|
84
|
+
vi.useFakeTimers()
|
|
85
|
+
const ac = new AbortController()
|
|
86
|
+
let done = false
|
|
87
|
+
const p = abortableSleep(100000, ac.signal).then(() => {
|
|
88
|
+
done = true
|
|
89
|
+
})
|
|
90
|
+
ac.abort()
|
|
91
|
+
await p
|
|
92
|
+
expect(done).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('isAbortError', () => {
|
|
97
|
+
it('reconnaît AbortError', () => {
|
|
98
|
+
expect(isAbortError({ name: 'AbortError' })).toBe(true)
|
|
99
|
+
expect(isAbortError(new DOMException('x', 'AbortError'))).toBe(true)
|
|
100
|
+
})
|
|
101
|
+
it('rejette le reste', () => {
|
|
102
|
+
expect(isAbortError(new Error('boom'))).toBe(false)
|
|
103
|
+
expect(isAbortError(null)).toBe(false)
|
|
104
|
+
expect(isAbortError('AbortError')).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { serializeBody } from '../src/serialize'
|
|
3
|
+
|
|
4
|
+
describe('serializeBody', () => {
|
|
5
|
+
it('sérialise un objet normal', () => {
|
|
6
|
+
expect(serializeBody({ a: 1, b: 'x' })).toBe('{"a":1,"b":"x"}')
|
|
7
|
+
})
|
|
8
|
+
it('convertit les bigint en string (pas de TypeError)', () => {
|
|
9
|
+
expect(serializeBody({ id: 10n })).toBe('{"id":"10"}')
|
|
10
|
+
expect(serializeBody({ big: 9007199254740993n })).toBe('{"big":"9007199254740993"}')
|
|
11
|
+
})
|
|
12
|
+
})
|