@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,481 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// E2E réel (stack live) — Lot storage-backend : valide les 4 correctifs backend.
|
|
3
|
+
// Réf. specs : docs/superpowers/specs/2026-07-13-storage-*-design.md
|
|
4
|
+
//
|
|
5
|
+
// Les méthodes SDK correspondantes (updateBucket, uploadToSignedUrl, copy/move
|
|
6
|
+
// cross-bucket) ne sont pas encore ajoutées (lot storage-sdk) → on teste le
|
|
7
|
+
// BACKEND via fetch REST direct (routes confirmées dans aura-storage/src/main.rs),
|
|
8
|
+
// avec la même mécanique de skip gracieux que storage-transform-e2e.test.ts.
|
|
9
|
+
//
|
|
10
|
+
// Scénarios :
|
|
11
|
+
// 1. img-transform-default : défaut déterministe = activé (plus de bascule
|
|
12
|
+
// autorisé→Forbidden à l'ouverture des settings).
|
|
13
|
+
// 2. signed-upload-url : mint service_role → PUT signé fonctionnel (valide
|
|
14
|
+
// aussi l'assomption gateway : PUT signé sans apikey passe le data-plane) ;
|
|
15
|
+
// PUT sans sig → 401 ; mint anon → 403 (sécurité #1) ; mint _derivatives → 403.
|
|
16
|
+
// 3. cache-hardening : x-aura-cache MISS→HIT + ETag stable ; overwrite source
|
|
17
|
+
// → nouveau rendu (clé versionnée etag) ; upload ?folder=_derivatives → 400.
|
|
18
|
+
// 4. cross-bucket : copy/move a→b (bucket destination distinct) ; dst inconnu → 404.
|
|
19
|
+
//
|
|
20
|
+
// Gaté par RUN_E2E + credentials.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest'
|
|
24
|
+
import { deflateSync } from 'node:zlib'
|
|
25
|
+
import { createAdminClient, createClient } from '../src/index'
|
|
26
|
+
import type { AuraResponse } from '../src/index'
|
|
27
|
+
|
|
28
|
+
const URL = process.env.AURA_E2E_URL
|
|
29
|
+
const PROJECT = process.env.AURA_E2E_PROJECT
|
|
30
|
+
const ANON = process.env.AURA_E2E_ANON_KEY
|
|
31
|
+
const SERVICE = process.env.AURA_E2E_SERVICE_KEY
|
|
32
|
+
const RUN = Boolean(process.env.RUN_E2E && URL && PROJECT && ANON && SERVICE)
|
|
33
|
+
|
|
34
|
+
const TRANSIENT_STATUS = new Set([408, 429, 502, 503, 504])
|
|
35
|
+
|
|
36
|
+
async function retryTransient<T>(fn: () => PromiseLike<AuraResponse<T>>): Promise<AuraResponse<T>> {
|
|
37
|
+
let res = await fn()
|
|
38
|
+
for (let i = 0; i < 10 && res.error && TRANSIENT_STATUS.has(res.error.status); i++) {
|
|
39
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
40
|
+
res = await fn()
|
|
41
|
+
}
|
|
42
|
+
return res
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function retryFetch(fn: () => Promise<Response>): Promise<Response> {
|
|
46
|
+
let res = await fn()
|
|
47
|
+
for (let i = 0; i < 10 && TRANSIENT_STATUS.has(res.status); i++) {
|
|
48
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
49
|
+
res = await fn()
|
|
50
|
+
}
|
|
51
|
+
return res
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Générateur PNG minimal (repris de storage-transform-e2e.test.ts) ──────────
|
|
55
|
+
const CRC_TABLE: Uint32Array = (() => {
|
|
56
|
+
const table = new Uint32Array(256)
|
|
57
|
+
for (let n = 0; n < 256; n++) {
|
|
58
|
+
let c = n
|
|
59
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
|
|
60
|
+
table[n] = c >>> 0
|
|
61
|
+
}
|
|
62
|
+
return table
|
|
63
|
+
})()
|
|
64
|
+
function crc32(bytes: Uint8Array): number {
|
|
65
|
+
let c = 0xffffffff
|
|
66
|
+
for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8)
|
|
67
|
+
return (c ^ 0xffffffff) >>> 0
|
|
68
|
+
}
|
|
69
|
+
function u32be(n: number): Uint8Array {
|
|
70
|
+
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff])
|
|
71
|
+
}
|
|
72
|
+
function concatBytes(...parts: Uint8Array[]): Uint8Array {
|
|
73
|
+
const total = parts.reduce((s, p) => s + p.length, 0)
|
|
74
|
+
const out = new Uint8Array(total)
|
|
75
|
+
let off = 0
|
|
76
|
+
for (const p of parts) {
|
|
77
|
+
out.set(p, off)
|
|
78
|
+
off += p.length
|
|
79
|
+
}
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
function pngChunk(type: string, data: Uint8Array): Uint8Array {
|
|
83
|
+
const typeBytes = new Uint8Array(type.split('').map((c) => c.charCodeAt(0)))
|
|
84
|
+
return concatBytes(u32be(data.length), typeBytes, data, u32be(crc32(concatBytes(typeBytes, data))))
|
|
85
|
+
}
|
|
86
|
+
function makePng(width: number, height: number, seedInit = 0x12345678): Uint8Array {
|
|
87
|
+
const SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])
|
|
88
|
+
const ihdr = pngChunk('IHDR', concatBytes(u32be(width), u32be(height), new Uint8Array([8, 2, 0, 0, 0])))
|
|
89
|
+
const stride = 1 + width * 3
|
|
90
|
+
const raw = new Uint8Array(stride * height)
|
|
91
|
+
let seed = seedInit
|
|
92
|
+
const nextByte = () => {
|
|
93
|
+
seed = (seed * 1103515245 + 12345) & 0x7fffffff
|
|
94
|
+
return (seed >>> 16) & 0xff
|
|
95
|
+
}
|
|
96
|
+
for (let y = 0; y < height; y++) {
|
|
97
|
+
const rowStart = y * stride
|
|
98
|
+
raw[rowStart] = 0
|
|
99
|
+
for (let x = 0; x < width; x++) {
|
|
100
|
+
const px = rowStart + 1 + x * 3
|
|
101
|
+
raw[px] = nextByte()
|
|
102
|
+
raw[px + 1] = nextByte()
|
|
103
|
+
raw[px + 2] = nextByte()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const compressed = deflateSync(raw)
|
|
107
|
+
const idat = pngChunk('IDAT', new Uint8Array(compressed.buffer, compressed.byteOffset, compressed.byteLength))
|
|
108
|
+
return concatBytes(SIGNATURE, ihdr, idat, pngChunk('IEND', new Uint8Array(0)))
|
|
109
|
+
}
|
|
110
|
+
function readPngWidth(bytes: Uint8Array): number {
|
|
111
|
+
return ((bytes[16] << 24) | (bytes[17] << 16) | (bytes[18] << 8) | bytes[19]) >>> 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Lit `{ data: { url } }` OU `{ url }` selon l'enveloppe ApiResponse. */
|
|
115
|
+
function extractUrl(json: unknown): string | undefined {
|
|
116
|
+
const j = json as { data?: { url?: string }; url?: string }
|
|
117
|
+
return j?.data?.url ?? j?.url
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe.runIf(RUN)('storage-backend E2E réel (stack live)', () => {
|
|
121
|
+
const suffix = () => process.env.AURA_E2E_SUFFIX ?? Math.random().toString(36).slice(2, 8)
|
|
122
|
+
const H_SERVICE = { apikey: SERVICE! } as Record<string, string>
|
|
123
|
+
|
|
124
|
+
// ── 1. img-transform-default : défaut déterministe activé ───────────────────
|
|
125
|
+
it(
|
|
126
|
+
'img_transform_enabled par défaut = activé : ouvrir les settings ne bascule pas transform en Forbidden',
|
|
127
|
+
async () => {
|
|
128
|
+
const admin = createAdminClient(URL!, PROJECT!, SERVICE!)
|
|
129
|
+
const bucket = `e2e-imgdef-${suffix()}`
|
|
130
|
+
let created = false
|
|
131
|
+
let uploadedKey = 'img.png'
|
|
132
|
+
try {
|
|
133
|
+
const createRes = await retryTransient(() =>
|
|
134
|
+
admin.storage.createBucket(bucket, { public: false, allowedMimeTypes: ['image/png'] }),
|
|
135
|
+
)
|
|
136
|
+
if (createRes.error && TRANSIENT_STATUS.has(createRes.error.status)) {
|
|
137
|
+
console.warn(`[storage-backend-e2e] SKIP img-default (createBucket ${createRes.error.status}).`)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
expect(createRes.error).toBeNull()
|
|
141
|
+
created = true
|
|
142
|
+
|
|
143
|
+
const uploadRes = await admin.storage.upload(bucket, 'img.png', new Blob([makePng(120, 120)], { type: 'image/png' }), {
|
|
144
|
+
contentType: 'image/png',
|
|
145
|
+
})
|
|
146
|
+
expect(uploadRes.error).toBeNull()
|
|
147
|
+
uploadedKey = uploadRes.data?.key ?? 'img.png'
|
|
148
|
+
|
|
149
|
+
// GET /settings crée la ligne (avec le fix : DEFAULT true + INSERT explicite true).
|
|
150
|
+
const settingsRes = await retryFetch(() =>
|
|
151
|
+
fetch(`${admin.baseUrl}/v1/storage/${admin.projectId}/settings`, { headers: H_SERVICE }),
|
|
152
|
+
)
|
|
153
|
+
if (!settingsRes.ok) {
|
|
154
|
+
console.warn(`[storage-backend-e2e] SKIP img-default (GET settings ${settingsRes.status}).`)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
const settingsJson = (await settingsRes.json()) as { data?: Record<string, unknown> }
|
|
158
|
+
const flag = (settingsJson.data ?? (settingsJson as Record<string, unknown>))['img_transform_enabled']
|
|
159
|
+
// Cœur du fix : après création de la ligne, le défaut est `true` (déterministe), donc pas
|
|
160
|
+
// de bascule autorisé→refusé. Sur le projet E2E partagé, un test concurrent peut avoir
|
|
161
|
+
// mis false explicitement → on n'assène l'assertion stricte que si le flag n'est pas false.
|
|
162
|
+
if (flag === false) {
|
|
163
|
+
console.warn('[storage-backend-e2e] img_transform_enabled=false (override explicite concurrent) — assertion défaut ignorée.')
|
|
164
|
+
} else {
|
|
165
|
+
expect(flag).toBe(true)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Et une transformation doit réussir (autorisée), après avoir touché les settings.
|
|
169
|
+
const dl = await retryTransient(() => admin.storage.download(bucket, uploadedKey, { transform: { width: 40 } }))
|
|
170
|
+
if (dl.error && TRANSIENT_STATUS.has(dl.error.status)) {
|
|
171
|
+
console.warn(`[storage-backend-e2e] SKIP img-default (download transform ${dl.error.status}).`)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
if (flag !== false) {
|
|
175
|
+
expect(dl.error).toBeNull()
|
|
176
|
+
const buf = new Uint8Array(await dl.data!.arrayBuffer())
|
|
177
|
+
expect(readPngWidth(buf)).toBe(40)
|
|
178
|
+
}
|
|
179
|
+
} finally {
|
|
180
|
+
if (created) {
|
|
181
|
+
try {
|
|
182
|
+
await admin.storage.remove(bucket, [uploadedKey])
|
|
183
|
+
} catch {
|
|
184
|
+
/* best-effort */
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
await admin.storage.deleteBucket(bucket)
|
|
188
|
+
} catch {
|
|
189
|
+
/* best-effort */
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
120_000,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
// ── 2. signed-upload-url : PUT signé + garde-fous sécurité ──────────────────
|
|
198
|
+
it(
|
|
199
|
+
'upload par URL signée : mint service_role → PUT fonctionnel ; PUT sans sig → 401 ; mint anon → 403 ; mint _derivatives → 403',
|
|
200
|
+
async () => {
|
|
201
|
+
const admin = createAdminClient(URL!, PROJECT!, SERVICE!)
|
|
202
|
+
const bucket = `e2e-signup-${suffix()}`
|
|
203
|
+
const key = `direct-${suffix()}.txt`
|
|
204
|
+
let created = false
|
|
205
|
+
try {
|
|
206
|
+
const createRes = await retryTransient(() =>
|
|
207
|
+
admin.storage.createBucket(bucket, { public: false, allowedMimeTypes: ['text/plain'] }),
|
|
208
|
+
)
|
|
209
|
+
if (createRes.error && TRANSIENT_STATUS.has(createRes.error.status)) {
|
|
210
|
+
console.warn(`[storage-backend-e2e] SKIP signed-upload (createBucket ${createRes.error.status}).`)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
expect(createRes.error).toBeNull()
|
|
214
|
+
created = true
|
|
215
|
+
|
|
216
|
+
const mintUrl = `${admin.baseUrl}/v1/storage/${admin.projectId}/${bucket}/upload-url?key=${encodeURIComponent(key)}`
|
|
217
|
+
|
|
218
|
+
// (a) Mint avec ANON (non service_role) → 403 (finding #1).
|
|
219
|
+
const anonMint = await retryFetch(() => fetch(mintUrl, { headers: { apikey: ANON! } }))
|
|
220
|
+
if (!TRANSIENT_STATUS.has(anonMint.status)) {
|
|
221
|
+
expect(anonMint.status).toBe(403)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// (b) Mint pour une clé sous _derivatives/ (service_role) → 403.
|
|
225
|
+
const derivMint = await retryFetch(() =>
|
|
226
|
+
fetch(
|
|
227
|
+
`${admin.baseUrl}/v1/storage/${admin.projectId}/${bucket}/upload-url?key=${encodeURIComponent('_derivatives/x/y.txt')}`,
|
|
228
|
+
{ headers: H_SERVICE },
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
if (!TRANSIENT_STATUS.has(derivMint.status)) {
|
|
232
|
+
expect(derivMint.status).toBe(403)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// (c) Mint service_role → URL signée.
|
|
236
|
+
const mintRes = await retryFetch(() => fetch(mintUrl, { headers: H_SERVICE }))
|
|
237
|
+
if (TRANSIENT_STATUS.has(mintRes.status)) {
|
|
238
|
+
console.warn(`[storage-backend-e2e] SKIP signed-upload (mint ${mintRes.status}).`)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
expect(mintRes.ok).toBe(true)
|
|
242
|
+
const signedUrl = extractUrl(await mintRes.json())
|
|
243
|
+
expect(signedUrl).toBeTruthy()
|
|
244
|
+
|
|
245
|
+
// (d) PUT du corps vers l'URL signée (SANS apikey) → 200. Valide l'assomption gateway.
|
|
246
|
+
const payload = `bonjour-${suffix()}`
|
|
247
|
+
const putRes = await retryFetch(() =>
|
|
248
|
+
fetch(signedUrl!, { method: 'PUT', headers: { 'content-type': 'text/plain' }, body: payload }),
|
|
249
|
+
)
|
|
250
|
+
if (TRANSIENT_STATUS.has(putRes.status)) {
|
|
251
|
+
console.warn(`[storage-backend-e2e] SKIP signed-upload (PUT ${putRes.status}).`)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
expect(
|
|
255
|
+
putRes.ok,
|
|
256
|
+
`PUT vers l'URL signée a échoué (${putRes.status}) — si 401/403 sans transitoire, la gateway ` +
|
|
257
|
+
`data-plane bloque le PUT signé non-authentifié (assomption gateway à corriger côté aura-gateway).`,
|
|
258
|
+
).toBe(true)
|
|
259
|
+
|
|
260
|
+
// (e) Contenu relu identique.
|
|
261
|
+
const dl = await retryTransient(() => admin.storage.download(bucket, key))
|
|
262
|
+
if (!(dl.error && TRANSIENT_STATUS.has(dl.error.status))) {
|
|
263
|
+
expect(dl.error).toBeNull()
|
|
264
|
+
expect(await dl.data!.text()).toBe(payload)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// (f) PUT sur le chemin objet SANS ?sig= → 401.
|
|
268
|
+
const noSig = await retryFetch(() =>
|
|
269
|
+
fetch(`${admin.baseUrl}/v1/storage/${admin.projectId}/${bucket}/${encodeURIComponent(key)}`, {
|
|
270
|
+
method: 'PUT',
|
|
271
|
+
headers: { 'content-type': 'text/plain', apikey: SERVICE! },
|
|
272
|
+
body: 'x',
|
|
273
|
+
}),
|
|
274
|
+
)
|
|
275
|
+
if (!TRANSIENT_STATUS.has(noSig.status)) {
|
|
276
|
+
expect(noSig.status).toBe(401)
|
|
277
|
+
}
|
|
278
|
+
} finally {
|
|
279
|
+
if (created) {
|
|
280
|
+
try {
|
|
281
|
+
await admin.storage.remove(bucket, [key])
|
|
282
|
+
} catch {
|
|
283
|
+
/* best-effort */
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
await admin.storage.deleteBucket(bucket)
|
|
287
|
+
} catch {
|
|
288
|
+
/* best-effort */
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
120_000,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
// ── 3. cache-hardening : ETag stable + overwrite + rejet _derivatives ───────
|
|
297
|
+
it(
|
|
298
|
+
'cache dérivés : MISS→HIT même ETag ; overwrite source → nouveau rendu ; upload ?folder=_derivatives → 400',
|
|
299
|
+
async () => {
|
|
300
|
+
const admin = createAdminClient(URL!, PROJECT!, SERVICE!)
|
|
301
|
+
const bucket = `e2e-cache-${suffix()}`
|
|
302
|
+
let created = false
|
|
303
|
+
let uploadedKey = 'img.png'
|
|
304
|
+
try {
|
|
305
|
+
const createRes = await retryTransient(() =>
|
|
306
|
+
admin.storage.createBucket(bucket, { public: false, allowedMimeTypes: ['image/png'] }),
|
|
307
|
+
)
|
|
308
|
+
if (createRes.error && TRANSIENT_STATUS.has(createRes.error.status)) {
|
|
309
|
+
console.warn(`[storage-backend-e2e] SKIP cache (createBucket ${createRes.error.status}).`)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
expect(createRes.error).toBeNull()
|
|
313
|
+
created = true
|
|
314
|
+
|
|
315
|
+
const up = await admin.storage.upload(bucket, 'img.png', new Blob([makePng(150, 150, 0x1111)], { type: 'image/png' }), {
|
|
316
|
+
contentType: 'image/png',
|
|
317
|
+
upsert: true,
|
|
318
|
+
})
|
|
319
|
+
expect(up.error).toBeNull()
|
|
320
|
+
uploadedKey = up.data?.key ?? 'img.png'
|
|
321
|
+
|
|
322
|
+
const signedRes = await retryTransient(() => admin.storage.createSignedUrl(bucket, uploadedKey, 600, { transform: { width: 45 } }))
|
|
323
|
+
if (signedRes.error && TRANSIENT_STATUS.has(signedRes.error.status)) {
|
|
324
|
+
console.warn(`[storage-backend-e2e] SKIP cache (signedUrl ${signedRes.error.status}).`)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
expect(signedRes.error).toBeNull()
|
|
328
|
+
const derivUrl = signedRes.data!.url
|
|
329
|
+
|
|
330
|
+
const f1 = await retryFetch(() => fetch(derivUrl))
|
|
331
|
+
if (TRANSIENT_STATUS.has(f1.status)) {
|
|
332
|
+
console.warn(`[storage-backend-e2e] SKIP cache (fetch1 ${f1.status}).`)
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
expect(f1.ok).toBe(true)
|
|
336
|
+
const etag1 = f1.headers.get('etag')
|
|
337
|
+
const cache1 = f1.headers.get('x-aura-cache')
|
|
338
|
+
await f1.arrayBuffer()
|
|
339
|
+
|
|
340
|
+
if (cache1 === 'MISS' && etag1) {
|
|
341
|
+
const f2 = await retryFetch(() => fetch(derivUrl))
|
|
342
|
+
if (!TRANSIENT_STATUS.has(f2.status)) {
|
|
343
|
+
expect(f2.headers.get('x-aura-cache')).toBe('HIT')
|
|
344
|
+
// ETag stable entre MISS et HIT (sous-finding c).
|
|
345
|
+
expect(f2.headers.get('etag')).toBe(etag1)
|
|
346
|
+
await f2.arrayBuffer()
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
console.warn(`[storage-backend-e2e] cache header x-aura-cache=${cache1} (non 'MISS') — assertion HIT/ETag ignorée.`)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Sous-finding (d) : le préfixe réservé _derivatives/ n'est écrivable que via un handler
|
|
353
|
+
// qui exploite `?folder=`. upload_file (POST .../{bucket}) IGNORE `?folder=` (sa clé = le
|
|
354
|
+
// nom de fichier, l'objet n'atterrit jamais sous _derivatives/) — le VRAI vecteur est
|
|
355
|
+
// init_multipart, qui construit la clé `{folder}/{uuid}`. On teste donc celui-là.
|
|
356
|
+
const folderRes = await retryFetch(() =>
|
|
357
|
+
fetch(
|
|
358
|
+
`${admin.baseUrl}/v1/storage/${admin.projectId}/${bucket}/multipart/init?filename=x.png&folder=${encodeURIComponent('_derivatives')}`,
|
|
359
|
+
{ method: 'POST', headers: H_SERVICE },
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
if (!TRANSIENT_STATUS.has(folderRes.status)) {
|
|
363
|
+
expect(
|
|
364
|
+
folderRes.status,
|
|
365
|
+
`init_multipart ?folder=_derivatives devrait être rejeté (400), reçu ${folderRes.status}`,
|
|
366
|
+
).toBe(400)
|
|
367
|
+
}
|
|
368
|
+
} finally {
|
|
369
|
+
if (created) {
|
|
370
|
+
try {
|
|
371
|
+
await admin.storage.remove(bucket, [uploadedKey])
|
|
372
|
+
} catch {
|
|
373
|
+
/* best-effort */
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
await admin.storage.deleteBucket(bucket)
|
|
377
|
+
} catch {
|
|
378
|
+
/* best-effort */
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
120_000,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
// ── 4. cross-bucket copy/move ───────────────────────────────────────────────
|
|
387
|
+
it(
|
|
388
|
+
'copy/move vers un bucket destination distinct ; dst_bucket inconnu → 404',
|
|
389
|
+
async () => {
|
|
390
|
+
const admin = createAdminClient(URL!, PROJECT!, SERVICE!)
|
|
391
|
+
const s = suffix()
|
|
392
|
+
const bucketA = `e2e-xb-a-${s}`
|
|
393
|
+
const bucketB = `e2e-xb-b-${s}`
|
|
394
|
+
let createdA = false
|
|
395
|
+
let createdB = false
|
|
396
|
+
let keyX = 'x.txt'
|
|
397
|
+
let keyY = 'y.txt'
|
|
398
|
+
try {
|
|
399
|
+
for (const [b, set] of [[bucketA, (v: boolean) => (createdA = v)], [bucketB, (v: boolean) => (createdB = v)]] as const) {
|
|
400
|
+
const r = await retryTransient(() => admin.storage.createBucket(b, { public: false, allowedMimeTypes: ['text/plain'] }))
|
|
401
|
+
if (r.error && TRANSIENT_STATUS.has(r.error.status)) {
|
|
402
|
+
console.warn(`[storage-backend-e2e] SKIP cross-bucket (createBucket ${b} ${r.error.status}).`)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
expect(r.error).toBeNull()
|
|
406
|
+
set(true)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const upX = await admin.storage.upload(bucketA, 'x.txt', new Blob(['contenu-x'], { type: 'text/plain' }), { contentType: 'text/plain' })
|
|
410
|
+
expect(upX.error).toBeNull()
|
|
411
|
+
keyX = upX.data?.key ?? 'x.txt'
|
|
412
|
+
const upY = await admin.storage.upload(bucketA, 'y.txt', new Blob(['contenu-y'], { type: 'text/plain' }), { contentType: 'text/plain' })
|
|
413
|
+
expect(upY.error).toBeNull()
|
|
414
|
+
keyY = upY.data?.key ?? 'y.txt'
|
|
415
|
+
|
|
416
|
+
// COPY a→b : objet créé dans b, source intacte.
|
|
417
|
+
const copyRes = await retryFetch(() =>
|
|
418
|
+
fetch(`${admin.baseUrl}/v1/storage/${admin.projectId}/${bucketA}/copy`, {
|
|
419
|
+
method: 'POST',
|
|
420
|
+
headers: { ...H_SERVICE, 'content-type': 'application/json' },
|
|
421
|
+
body: JSON.stringify({ src_key: keyX, dst_key: 'copied.txt', dst_bucket: bucketB }),
|
|
422
|
+
}),
|
|
423
|
+
)
|
|
424
|
+
if (TRANSIENT_STATUS.has(copyRes.status)) {
|
|
425
|
+
console.warn(`[storage-backend-e2e] SKIP cross-bucket (copy ${copyRes.status}).`)
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
expect(copyRes.ok, `copy a→b devrait réussir, reçu ${copyRes.status}`).toBe(true)
|
|
429
|
+
const dlCopied = await retryTransient(() => admin.storage.download(bucketB, 'copied.txt'))
|
|
430
|
+
if (!(dlCopied.error && TRANSIENT_STATUS.has(dlCopied.error.status))) {
|
|
431
|
+
expect(dlCopied.error).toBeNull()
|
|
432
|
+
expect(await dlCopied.data!.text()).toBe('contenu-x')
|
|
433
|
+
}
|
|
434
|
+
const dlSrc = await retryTransient(() => admin.storage.download(bucketA, keyX))
|
|
435
|
+
if (!(dlSrc.error && TRANSIENT_STATUS.has(dlSrc.error.status))) {
|
|
436
|
+
expect(dlSrc.error, 'la source doit rester intacte après copy').toBeNull()
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// MOVE a→b : objet dans b, absent de a.
|
|
440
|
+
const moveRes = await retryFetch(() =>
|
|
441
|
+
fetch(`${admin.baseUrl}/v1/storage/${admin.projectId}/${bucketA}/move`, {
|
|
442
|
+
method: 'POST',
|
|
443
|
+
headers: { ...H_SERVICE, 'content-type': 'application/json' },
|
|
444
|
+
body: JSON.stringify({ src_key: keyY, dst_key: 'moved.txt', dst_bucket: bucketB }),
|
|
445
|
+
}),
|
|
446
|
+
)
|
|
447
|
+
if (!TRANSIENT_STATUS.has(moveRes.status)) {
|
|
448
|
+
expect(moveRes.ok, `move a→b devrait réussir, reçu ${moveRes.status}`).toBe(true)
|
|
449
|
+
const dlMoved = await retryTransient(() => admin.storage.download(bucketB, 'moved.txt'))
|
|
450
|
+
if (!(dlMoved.error && TRANSIENT_STATUS.has(dlMoved.error.status))) {
|
|
451
|
+
expect(dlMoved.error).toBeNull()
|
|
452
|
+
expect(await dlMoved.data!.text()).toBe('contenu-y')
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// dst_bucket inexistant → 404.
|
|
457
|
+
const badRes = await retryFetch(() =>
|
|
458
|
+
fetch(`${admin.baseUrl}/v1/storage/${admin.projectId}/${bucketA}/copy`, {
|
|
459
|
+
method: 'POST',
|
|
460
|
+
headers: { ...H_SERVICE, 'content-type': 'application/json' },
|
|
461
|
+
body: JSON.stringify({ src_key: keyX, dst_key: 'z.txt', dst_bucket: `nope-${s}` }),
|
|
462
|
+
}),
|
|
463
|
+
)
|
|
464
|
+
if (!TRANSIENT_STATUS.has(badRes.status)) {
|
|
465
|
+
expect(badRes.status, 'copy vers un bucket inconnu doit renvoyer 404').toBe(404)
|
|
466
|
+
}
|
|
467
|
+
} finally {
|
|
468
|
+
for (const [b, wasCreated] of [[bucketA, createdA], [bucketB, createdB]] as const) {
|
|
469
|
+
if (wasCreated) {
|
|
470
|
+
try {
|
|
471
|
+
await admin.storage.deleteBucket(b)
|
|
472
|
+
} catch {
|
|
473
|
+
/* best-effort */
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
120_000,
|
|
480
|
+
)
|
|
481
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// E2E réel (stack live) — Lot sdk-storage-gen / `storage-sdk-parity-quickwins`.
|
|
3
|
+
// Réf. : docs/superpowers/specs/2026-07-13-storage-sdk-parity-quickwins-design.md
|
|
4
|
+
//
|
|
5
|
+
// Vérifie updateBucket (PATCH partiel), option upload `metadata` (relue via
|
|
6
|
+
// getMetadata), createSignedUrls (batch fan-out). Gaté RUN_E2E, skip gracieux.
|
|
7
|
+
//
|
|
8
|
+
// NB clé : `upload_file` assigne une clé UUID et la RENVOIE (il n'utilise le
|
|
9
|
+
// chemin client que pour l'extension). Les lectures (getMetadata/createSignedUrl)
|
|
10
|
+
// DOIVENT donc utiliser `up.data.key`, jamais le chemin logique d'origine.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from 'vitest'
|
|
14
|
+
import { createAdminClient } from '../src/index'
|
|
15
|
+
import type { AuraResponse } from '../src/index'
|
|
16
|
+
|
|
17
|
+
const URL = process.env.AURA_E2E_URL
|
|
18
|
+
const PROJECT = process.env.AURA_E2E_PROJECT
|
|
19
|
+
const SERVICE = process.env.AURA_E2E_SERVICE_KEY
|
|
20
|
+
const RUN = Boolean(process.env.RUN_E2E && URL && PROJECT && SERVICE)
|
|
21
|
+
const TRANSIENT = new Set([408, 429, 502, 503, 504])
|
|
22
|
+
|
|
23
|
+
async function retry<T>(fn: () => Promise<AuraResponse<T>>): Promise<AuraResponse<T>> {
|
|
24
|
+
let r = await fn()
|
|
25
|
+
for (let i = 0; i < 8 && r.error && TRANSIENT.has(r.error.status); i++) {
|
|
26
|
+
await new Promise((res) => setTimeout(res, 1000))
|
|
27
|
+
r = await fn()
|
|
28
|
+
}
|
|
29
|
+
return r
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe.runIf(RUN)('storage SDK parity E2E réel (updateBucket / metadata / createSignedUrls)', () => {
|
|
33
|
+
it(
|
|
34
|
+
'updateBucket rend public ; upload metadata est relu ; createSignedUrls renvoie N URLs',
|
|
35
|
+
async () => {
|
|
36
|
+
const admin = createAdminClient(URL!, PROJECT!, SERVICE!)
|
|
37
|
+
const suffix = process.env.AURA_E2E_SUFFIX ?? Math.random().toString(36).slice(2, 8)
|
|
38
|
+
const bucket = `sbp${suffix}`
|
|
39
|
+
const tag = `v-${suffix}`
|
|
40
|
+
let created = false
|
|
41
|
+
const keys: string[] = []
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const cb = await retry(() => admin.storage.createBucket(bucket, { public: false }))
|
|
45
|
+
if (cb.error && TRANSIENT.has(cb.error.status)) {
|
|
46
|
+
console.warn(`[storage-sdk-parity-e2e] SKIP (createBucket ${cb.error.status}).`)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
expect(cb.error, `createBucket: ${JSON.stringify(cb.error)}`).toBeNull()
|
|
50
|
+
created = true
|
|
51
|
+
|
|
52
|
+
// --- updateBucket (PATCH partiel) : privé → public -------------------
|
|
53
|
+
const ub = await retry(() =>
|
|
54
|
+
admin.storage.updateBucket(bucket, { public: true, fileSizeLimit: 5_000_000 }),
|
|
55
|
+
)
|
|
56
|
+
expect(ub.error, `updateBucket: ${JSON.stringify(ub.error)}`).toBeNull()
|
|
57
|
+
const gb = await retry(() => admin.storage.getBucket(bucket))
|
|
58
|
+
expect(gb.error).toBeNull()
|
|
59
|
+
expect((gb.data as { public?: boolean } | null)?.public, 'bucket doit être public après updateBucket').toBe(true)
|
|
60
|
+
|
|
61
|
+
// --- upload avec metadata → relue via getMetadata (clé RETOURNÉE) ----
|
|
62
|
+
const up = await retry(() =>
|
|
63
|
+
admin.storage.upload(bucket, 'f.txt', new Blob(['hello']), {
|
|
64
|
+
contentType: 'text/plain',
|
|
65
|
+
metadata: { tag },
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
expect(up.error, `upload metadata: ${JSON.stringify(up.error)}`).toBeNull()
|
|
69
|
+
const keyF = (up.data as { key?: string } | null)?.key
|
|
70
|
+
expect(keyF, 'upload doit retourner la clé stockée').toBeTruthy()
|
|
71
|
+
keys.push(keyF!)
|
|
72
|
+
|
|
73
|
+
const gm = await retry(() => admin.storage.getMetadata(bucket, keyF!))
|
|
74
|
+
expect(gm.error, `getMetadata(${keyF}): ${JSON.stringify(gm.error)}`).toBeNull()
|
|
75
|
+
expect(JSON.stringify(gm.data ?? {}), 'la metadata custom (tag) doit être relue').toContain(tag)
|
|
76
|
+
|
|
77
|
+
// --- createSignedUrls (batch) sur les clés réelles ------------------
|
|
78
|
+
const up2 = await retry(() =>
|
|
79
|
+
admin.storage.upload(bucket, 'g.txt', new Blob(['g']), { contentType: 'text/plain' }),
|
|
80
|
+
)
|
|
81
|
+
expect(up2.error).toBeNull()
|
|
82
|
+
const keyG = (up2.data as { key?: string } | null)?.key
|
|
83
|
+
expect(keyG).toBeTruthy()
|
|
84
|
+
keys.push(keyG!)
|
|
85
|
+
|
|
86
|
+
const su = await retry(() => admin.storage.createSignedUrls(bucket, [keyF!, keyG!], 300))
|
|
87
|
+
expect(su.error, `createSignedUrls: ${JSON.stringify(su.error)}`).toBeNull()
|
|
88
|
+
const rows = su.data ?? []
|
|
89
|
+
expect(rows.length, 'createSignedUrls doit renvoyer 2 entrées').toBe(2)
|
|
90
|
+
expect(rows.every((r) => 'url' in r && typeof r.url === 'string' && r.url.length > 0), 'chaque entrée doit porter une URL').toBe(true)
|
|
91
|
+
} finally {
|
|
92
|
+
if (created) {
|
|
93
|
+
try {
|
|
94
|
+
if (keys.length) await admin.storage.remove(bucket, keys)
|
|
95
|
+
await admin.storage.deleteBucket(bucket)
|
|
96
|
+
} catch {
|
|
97
|
+
/* best-effort */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
90_000,
|
|
103
|
+
)
|
|
104
|
+
})
|