@mycadet/assistant 0.12.11 → 0.13.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/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/openapi.yaml +2 -708
- package/package.json +2 -3
- package/scripts/sync-web-search-catalog.ts +1 -2
- package/src/__tests__/cloud-profile-guard.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +6 -38
- package/src/__tests__/conversation-queue.test.ts +45 -6
- package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
- package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
- package/src/__tests__/persona-resolver.test.ts +0 -51
- package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
- package/src/__tests__/run-due-schedules.test.ts +0 -4
- package/src/__tests__/schedule-store.test.ts +0 -12
- package/src/__tests__/stt-stream-session.test.ts +4 -4
- package/src/__tests__/telephony-tts-guard.test.ts +2 -50
- package/src/__tests__/voice-config-update.test.ts +12 -167
- package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
- package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
- package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
- package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
- package/src/calls/resolve-call-tts-provider.ts +6 -25
- package/src/calls/telephony-credential-preflight.ts +0 -10
- package/src/calls/telephony-tts-capability.ts +1 -17
- package/src/cli/commands/tts.ts +1 -2
- package/src/cli/index.help.ts +0 -2
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
- package/src/config/schemas/__tests__/stt.test.ts +10 -14
- package/src/config/schemas/services.ts +5 -6
- package/src/config/schemas/stt.ts +1 -3
- package/src/config/schemas/tts.ts +1 -30
- package/src/daemon/daemon-readiness.ts +4 -29
- package/src/daemon/lifecycle.ts +0 -31
- package/src/daemon/providers-setup.ts +0 -8
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -66
- package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
- package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
- package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
- package/src/live-voice/live-voice-credential-preflight.ts +9 -33
- package/src/live-voice/live-voice-session.ts +10 -6
- package/src/live-voice/live-voice-tts.ts +1 -8
- package/src/plugins/defaults/memory/worker.ts +3 -3
- package/src/prompts/persona-resolver.ts +0 -35
- package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
- package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
- package/src/providers/search-provider-catalog.ts +7 -39
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
- package/src/providers/speech-to-text/provider-catalog.ts +2 -73
- package/src/providers/speech-to-text/resolve.ts +3 -120
- package/src/runtime/http-router-types.ts +1 -3
- package/src/runtime/http-server.ts +1 -1
- package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
- package/src/runtime/routes/index.ts +0 -4
- package/src/runtime/routes/live-voice-routes.test.ts +4 -11
- package/src/runtime/routes/live-voice-routes.ts +8 -11
- package/src/runtime/routes/secret-routes.ts +0 -6
- package/src/runtime/routes/tts-routes.ts +0 -38
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
- package/src/schedule/schedule-store.ts +0 -7
- package/src/schedule/scheduler.ts +0 -5
- package/src/schedule/worker.ts +1 -1
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
- package/src/stt/daemon-batch-transcriber.ts +4 -38
- package/src/stt/types.ts +1 -9
- package/src/tools/network/__tests__/web-search.test.ts +3 -250
- package/src/tools/network/web-search.ts +4 -165
- package/src/tts/__tests__/provider-resolution.test.ts +0 -5
- package/src/tts/provider-catalog.ts +0 -4
- package/src/tts/types.ts +0 -1
- package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
- package/src/__tests__/migration-export-http.test.ts +0 -557
- package/src/__tests__/migration-export-streaming.test.ts +0 -350
- package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
- package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
- package/src/__tests__/migration-import-from-path.test.ts +0 -324
- package/src/__tests__/migration-import-from-url.test.ts +0 -588
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
- package/src/__tests__/migration-jobs-status.test.ts +0 -139
- package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
- package/src/__tests__/migration-transport.test.ts +0 -1089
- package/src/__tests__/migration-validate-http.test.ts +0 -634
- package/src/__tests__/migration-wizard.test.ts +0 -1395
- package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
- package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
- package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
- package/src/__tests__/validation-results-screen.test.ts +0 -1130
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
- package/src/background-wake/background-wake-routes.test.ts +0 -906
- package/src/background-wake/cloud-client.test.ts +0 -328
- package/src/background-wake/cloud-client.ts +0 -196
- package/src/background-wake/next-wake.test.ts +0 -335
- package/src/background-wake/next-wake.ts +0 -188
- package/src/background-wake/publisher.ts +0 -93
- package/src/background-wake/wake-intent-hooks.test.ts +0 -234
- package/src/cli/commands/__tests__/backup.test.ts +0 -363
- package/src/cli/commands/backup.help.ts +0 -180
- package/src/cli/commands/backup.ts +0 -398
- package/src/cloud/cloud-speech.test.ts +0 -335
- package/src/cloud/cloud-speech.ts +0 -300
- package/src/config/cloud-speech-defaults.ts +0 -300
- package/src/config/sanitize-for-transfer.ts +0 -111
- package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
- package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
- package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
- package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
- package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
- package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
- package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
- package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
- package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
- package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
- package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
- package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
- package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
- package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
- package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
- package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
- package/src/runtime/migrations/gcs-signed-url.ts +0 -190
- package/src/runtime/migrations/job-registry.ts +0 -297
- package/src/runtime/migrations/migration-transport.ts +0 -612
- package/src/runtime/migrations/migration-wizard.ts +0 -747
- package/src/runtime/migrations/origin-mode.ts +0 -42
- package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
- package/src/runtime/migrations/staged-import-path.ts +0 -116
- package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
- package/src/runtime/migrations/validation-results-screen.ts +0 -469
- package/src/runtime/migrations/vbundle-builder.ts +0 -1382
- package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
- package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
- package/src/runtime/migrations/vbundle-importer.ts +0 -826
- package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
- package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
- package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
- package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
- package/src/runtime/migrations/vbundle-validator.ts +0 -724
- package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
- package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
- package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
- package/src/runtime/routes/background-wake-routes.ts +0 -387
- package/src/runtime/routes/migration-routes.ts +0 -2584
- package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
- package/src/tools/network/cloud-search-proxy.ts +0 -211
- package/src/tts/__tests__/cadet-provider.test.ts +0 -253
- package/src/tts/providers/cadet-provider.ts +0 -244
- package/src/types/tar-stream.d.ts +0 -66
|
@@ -1,706 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for POST /v1/migrations/export-to-gcs.
|
|
3
|
-
*
|
|
4
|
-
* Covered:
|
|
5
|
-
* - Happy path: the handler returns 202 + job_id, the background job
|
|
6
|
-
* PUTs a valid .vbundle to the fixture server, and the final job
|
|
7
|
-
* record carries a matching result payload.
|
|
8
|
-
* - Concurrency: a second export-to-gcs while the first is still
|
|
9
|
-
* pending/running returns 409 `export_in_progress`.
|
|
10
|
-
* - Upload failure: when the PUT server responds 500, the job ends
|
|
11
|
-
* `failed` with `error.code === "upload_failed"` and
|
|
12
|
-
* `upstreamStatus: 500`.
|
|
13
|
-
* - Invalid URL: non-https, wrong host, and path-traversal URLs are
|
|
14
|
-
* rejected at the handler (400) with the validator's reason code
|
|
15
|
-
* in `error.reason`.
|
|
16
|
-
*
|
|
17
|
-
* The .vbundle bytes observed by the fixture server are cross-checked
|
|
18
|
-
* against `validateVBundle` to prove the upload body matches what
|
|
19
|
-
* `streamExportVBundle()` would produce.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
23
|
-
import { createServer, type Server } from "node:http";
|
|
24
|
-
import { join } from "node:path";
|
|
25
|
-
import {
|
|
26
|
-
afterAll,
|
|
27
|
-
afterEach,
|
|
28
|
-
beforeAll,
|
|
29
|
-
describe,
|
|
30
|
-
expect,
|
|
31
|
-
mock,
|
|
32
|
-
test,
|
|
33
|
-
} from "bun:test";
|
|
34
|
-
|
|
35
|
-
const testDir = process.env.CADET_WORKSPACE_DIR!;
|
|
36
|
-
const testDbDir = join(testDir, "data", "db");
|
|
37
|
-
const testDbPath = join(testDbDir, "assistant.db");
|
|
38
|
-
const testConfigPath = join(testDir, "config.json");
|
|
39
|
-
|
|
40
|
-
// The gateway's debug export arrives over IPC as a base64 tar.gz. Stand in
|
|
41
|
-
// for the socket so the debug profile can be exercised without a gateway.
|
|
42
|
-
const FAKE_GATEWAY_ARCHIVE = Buffer.from("not really a tar.gz");
|
|
43
|
-
const gatewayDebugExportMock = mock(async () => ({
|
|
44
|
-
ok: true as const,
|
|
45
|
-
archive_base64: FAKE_GATEWAY_ARCHIVE.toString("base64"),
|
|
46
|
-
size_bytes: FAKE_GATEWAY_ARCHIVE.length,
|
|
47
|
-
}));
|
|
48
|
-
mock.module("../ipc/gateway-client.js", () => ({
|
|
49
|
-
ipcCallPersistent: (method: string) => {
|
|
50
|
-
if (method !== "gateway_debug_export") {
|
|
51
|
-
throw new Error(`unexpected IPC method ${method}`);
|
|
52
|
-
}
|
|
53
|
-
return gatewayDebugExportMock();
|
|
54
|
-
},
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
// Force the credential collector onto the unreachable branch so the
|
|
58
|
-
// handler treats it as "export without credentials" (fast, no CES).
|
|
59
|
-
// The REAL secure-keys module pinned to a faultable memory backend with the
|
|
60
|
-
// whole vault faulted: the handler treats it as "export without credentials"
|
|
61
|
-
// (fast, no CES).
|
|
62
|
-
import { useFaultableSecureKeyMap } from "./helpers/secure-keys.js";
|
|
63
|
-
|
|
64
|
-
const { setAllUnreachable } = useFaultableSecureKeyMap();
|
|
65
|
-
setAllUnreachable(true);
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// Imports (after mocks so module-level code picks up the stubs)
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
import { migrationJobs } from "../runtime/migrations/job-registry.js";
|
|
72
|
-
import { validateVBundle } from "../runtime/migrations/vbundle-validator.js";
|
|
73
|
-
import {
|
|
74
|
-
_setUrlImportValidatorOptionsForTests,
|
|
75
|
-
handleMigrationExportToGcs,
|
|
76
|
-
} from "../runtime/routes/migration-routes.js";
|
|
77
|
-
import { callHandler } from "./helpers/call-route-handler.js";
|
|
78
|
-
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// Fixture workspace: write a minimal SQLite file + config so the exporter
|
|
81
|
-
// has real data to walk.
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
|
|
84
|
-
const SQLITE_HEADER = new Uint8Array([
|
|
85
|
-
0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74,
|
|
86
|
-
0x20, 0x33, 0x00,
|
|
87
|
-
]);
|
|
88
|
-
const TEST_CONFIG = { provider: "anthropic", model: "test-model" };
|
|
89
|
-
|
|
90
|
-
beforeAll(() => {
|
|
91
|
-
mkdirSync(testDbDir, { recursive: true });
|
|
92
|
-
writeFileSync(testDbPath, SQLITE_HEADER);
|
|
93
|
-
writeFileSync(testConfigPath, JSON.stringify(TEST_CONFIG, null, 2));
|
|
94
|
-
|
|
95
|
-
// Widen the URL validator allowlist so the handler accepts a URL
|
|
96
|
-
// pointing at the local fixture server.
|
|
97
|
-
_setUrlImportValidatorOptionsForTests({
|
|
98
|
-
allowedHosts: ["127.0.0.1", "storage.googleapis.com"],
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
afterAll(() => {
|
|
103
|
-
_setUrlImportValidatorOptionsForTests(undefined);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Drop stale job records between tests so concurrency assertions are
|
|
107
|
-
// deterministic (a `failed` job from a previous test must not collide
|
|
108
|
-
// with the next test's `startJob("export", …)` call — the registry only
|
|
109
|
-
// rejects while the prior job is still pending/running, but evicting the
|
|
110
|
-
// entry defensively keeps log output clean too).
|
|
111
|
-
afterEach(async () => {
|
|
112
|
-
// Wait a tick so any in-flight runners can finalize their state.
|
|
113
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
114
|
-
// Force-evict any remaining terminal records by tightening the TTL.
|
|
115
|
-
const savedTtl = migrationJobs.completedJobTtlMs;
|
|
116
|
-
migrationJobs.completedJobTtlMs = 0;
|
|
117
|
-
migrationJobs.sweep();
|
|
118
|
-
migrationJobs.completedJobTtlMs = savedTtl;
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
// Local http fixture server that captures PUT bodies
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
|
|
125
|
-
interface FixtureServer {
|
|
126
|
-
server: Server;
|
|
127
|
-
port: number;
|
|
128
|
-
close: () => Promise<void>;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function startFixtureServer(
|
|
132
|
-
handler: (
|
|
133
|
-
req: import("node:http").IncomingMessage,
|
|
134
|
-
res: import("node:http").ServerResponse,
|
|
135
|
-
) => void,
|
|
136
|
-
): Promise<FixtureServer> {
|
|
137
|
-
const server = createServer(handler);
|
|
138
|
-
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
139
|
-
const address = server.address();
|
|
140
|
-
if (!address || typeof address === "string") {
|
|
141
|
-
throw new Error("fixture server bound to unexpected address");
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
server,
|
|
145
|
-
port: address.port,
|
|
146
|
-
close: async () => {
|
|
147
|
-
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function makeFakeSignedUploadUrl(port: number): string {
|
|
153
|
-
// `validateGcsSignedUrl` requires a signature query param; pin a dummy.
|
|
154
|
-
return `http://127.0.0.1:${port}/upload/bundle?X-Goog-Signature=fake`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Read the full body of an incoming request into a Buffer. */
|
|
158
|
-
function collectBody(
|
|
159
|
-
req: import("node:http").IncomingMessage,
|
|
160
|
-
): Promise<Buffer> {
|
|
161
|
-
return new Promise((resolve, reject) => {
|
|
162
|
-
const chunks: Buffer[] = [];
|
|
163
|
-
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
164
|
-
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
165
|
-
req.on("error", reject);
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Poll `migrationJobs.getJob(id)` until the job reaches a terminal state. */
|
|
170
|
-
async function waitForJobTerminal(
|
|
171
|
-
id: string,
|
|
172
|
-
{ timeoutMs = 10_000 }: { timeoutMs?: number } = {},
|
|
173
|
-
): Promise<NonNullable<ReturnType<typeof migrationJobs.getJob>>> {
|
|
174
|
-
const deadline = Date.now() + timeoutMs;
|
|
175
|
-
while (Date.now() < deadline) {
|
|
176
|
-
const job = migrationJobs.getJob(id);
|
|
177
|
-
if (job && (job.status === "complete" || job.status === "failed")) {
|
|
178
|
-
return job;
|
|
179
|
-
}
|
|
180
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
181
|
-
}
|
|
182
|
-
throw new Error(`Job ${id} did not reach a terminal state in time`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
// Response shape types
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
|
|
189
|
-
interface AcceptedResponse {
|
|
190
|
-
job_id: string;
|
|
191
|
-
status: "pending";
|
|
192
|
-
type: "export";
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
interface ErrorEnvelope {
|
|
196
|
-
error: {
|
|
197
|
-
code: string;
|
|
198
|
-
reason?: string;
|
|
199
|
-
job_id?: string;
|
|
200
|
-
message?: string;
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ---------------------------------------------------------------------------
|
|
205
|
-
// Tests
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
|
|
208
|
-
describe("handleMigrationExportToGcs — happy path", () => {
|
|
209
|
-
test("202 + job completes with a valid .vbundle PUT to the upload URL", async () => {
|
|
210
|
-
let capturedBody: Buffer | undefined;
|
|
211
|
-
let capturedContentType: string | undefined;
|
|
212
|
-
|
|
213
|
-
const fixture = await startFixtureServer(async (req, res) => {
|
|
214
|
-
if (req.method !== "PUT") {
|
|
215
|
-
res.writeHead(405);
|
|
216
|
-
res.end();
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
capturedContentType = req.headers["content-type"];
|
|
220
|
-
capturedBody = await collectBody(req);
|
|
221
|
-
res.writeHead(200);
|
|
222
|
-
res.end();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
const req = new Request("http://localhost/v1/migrations/export-to-gcs", {
|
|
227
|
-
method: "POST",
|
|
228
|
-
headers: { "Content-Type": "application/json" },
|
|
229
|
-
body: JSON.stringify({
|
|
230
|
-
upload_url: makeFakeSignedUploadUrl(fixture.port),
|
|
231
|
-
description: "happy-path export",
|
|
232
|
-
}),
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const res = await callHandler(
|
|
236
|
-
handleMigrationExportToGcs,
|
|
237
|
-
req,
|
|
238
|
-
undefined,
|
|
239
|
-
202,
|
|
240
|
-
);
|
|
241
|
-
expect(res.status).toBe(202);
|
|
242
|
-
|
|
243
|
-
const body = (await res.json()) as AcceptedResponse;
|
|
244
|
-
expect(body.status).toBe("pending");
|
|
245
|
-
expect(body.type).toBe("export");
|
|
246
|
-
expect(body.job_id).toBeDefined();
|
|
247
|
-
expect(typeof body.job_id).toBe("string");
|
|
248
|
-
|
|
249
|
-
const terminal = await waitForJobTerminal(body.job_id);
|
|
250
|
-
expect(terminal.status).toBe("complete");
|
|
251
|
-
expect(terminal.error).toBeUndefined();
|
|
252
|
-
|
|
253
|
-
const result = terminal.result as {
|
|
254
|
-
size: number;
|
|
255
|
-
sha256: string;
|
|
256
|
-
schemaVersion: string;
|
|
257
|
-
credentialsIncluded: number;
|
|
258
|
-
};
|
|
259
|
-
expect(result.size).toBeGreaterThan(0);
|
|
260
|
-
expect(typeof result.sha256).toBe("string");
|
|
261
|
-
expect(result.sha256.length).toBeGreaterThan(0);
|
|
262
|
-
expect(result.schemaVersion).toBeDefined();
|
|
263
|
-
expect(result.credentialsIncluded).toBe(0);
|
|
264
|
-
|
|
265
|
-
expect(capturedContentType).toBe("application/octet-stream");
|
|
266
|
-
expect(capturedBody).toBeDefined();
|
|
267
|
-
expect(capturedBody!.byteLength).toBe(result.size);
|
|
268
|
-
|
|
269
|
-
// The uploaded body is a real .vbundle — validate it.
|
|
270
|
-
const validation = validateVBundle(new Uint8Array(capturedBody!));
|
|
271
|
-
expect(validation.is_valid).toBe(true);
|
|
272
|
-
expect(validation.errors).toHaveLength(0);
|
|
273
|
-
expect(validation.manifest?.checksum).toBe(result.sha256);
|
|
274
|
-
} finally {
|
|
275
|
-
await fixture.close();
|
|
276
|
-
}
|
|
277
|
-
}, 15_000);
|
|
278
|
-
|
|
279
|
-
test("managed origin skips credential collection and redacts secrets", async () => {
|
|
280
|
-
// The secure-keys mock reports the store unreachable, which normally
|
|
281
|
-
// forces `secrets_redacted: false` (we can't prove the bundle is clean).
|
|
282
|
-
// In managed mode the handler must skip collection entirely — the
|
|
283
|
-
// manifest schema refuses managed bundles carrying secrets — so the
|
|
284
|
-
// manifest claims a truthful `secrets_redacted: true` regardless of
|
|
285
|
-
// credential-store state, and the bundle passes validation with
|
|
286
|
-
// `origin.mode: "managed"`.
|
|
287
|
-
const originalIsPlatform = process.env.IS_CLOUD;
|
|
288
|
-
process.env.IS_CLOUD = "true";
|
|
289
|
-
|
|
290
|
-
let capturedBody: Buffer | undefined;
|
|
291
|
-
const fixture = await startFixtureServer(async (req, res) => {
|
|
292
|
-
if (req.method !== "PUT") {
|
|
293
|
-
res.writeHead(405);
|
|
294
|
-
res.end();
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
capturedBody = await collectBody(req);
|
|
298
|
-
res.writeHead(200);
|
|
299
|
-
res.end();
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
const req = new Request("http://localhost/v1/migrations/export-to-gcs", {
|
|
304
|
-
method: "POST",
|
|
305
|
-
headers: { "Content-Type": "application/json" },
|
|
306
|
-
body: JSON.stringify({
|
|
307
|
-
upload_url: makeFakeSignedUploadUrl(fixture.port),
|
|
308
|
-
description: "managed export",
|
|
309
|
-
}),
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
const res = await callHandler(
|
|
313
|
-
handleMigrationExportToGcs,
|
|
314
|
-
req,
|
|
315
|
-
undefined,
|
|
316
|
-
202,
|
|
317
|
-
);
|
|
318
|
-
const body = (await res.json()) as AcceptedResponse;
|
|
319
|
-
const terminal = await waitForJobTerminal(body.job_id);
|
|
320
|
-
expect(terminal.status).toBe("complete");
|
|
321
|
-
|
|
322
|
-
const result = terminal.result as { credentialsIncluded: number };
|
|
323
|
-
expect(result.credentialsIncluded).toBe(0);
|
|
324
|
-
|
|
325
|
-
const validation = validateVBundle(new Uint8Array(capturedBody!));
|
|
326
|
-
expect(validation.is_valid).toBe(true);
|
|
327
|
-
expect(validation.manifest?.origin.mode).toBe("managed");
|
|
328
|
-
expect(validation.manifest?.secrets_redacted).toBe(true);
|
|
329
|
-
} finally {
|
|
330
|
-
if (originalIsPlatform === undefined) {
|
|
331
|
-
delete process.env.IS_CLOUD;
|
|
332
|
-
} else {
|
|
333
|
-
process.env.IS_CLOUD = originalIsPlatform;
|
|
334
|
-
}
|
|
335
|
-
await fixture.close();
|
|
336
|
-
}
|
|
337
|
-
}, 15_000);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
describe("handleMigrationExportToGcs — debug profile", () => {
|
|
341
|
-
test("adds the gateway's archive under gateway/ and marks the manifest", async () => {
|
|
342
|
-
gatewayDebugExportMock.mockClear();
|
|
343
|
-
let capturedBody: Buffer | undefined;
|
|
344
|
-
const fixture = await startFixtureServer(async (req, res) => {
|
|
345
|
-
capturedBody = await collectBody(req);
|
|
346
|
-
res.writeHead(200);
|
|
347
|
-
res.end();
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
const req = new Request("http://localhost/v1/migrations/export-to-gcs", {
|
|
352
|
-
method: "POST",
|
|
353
|
-
headers: { "Content-Type": "application/json" },
|
|
354
|
-
body: JSON.stringify({
|
|
355
|
-
upload_url: makeFakeSignedUploadUrl(fixture.port),
|
|
356
|
-
profile: "debug",
|
|
357
|
-
}),
|
|
358
|
-
});
|
|
359
|
-
const res = await callHandler(
|
|
360
|
-
handleMigrationExportToGcs,
|
|
361
|
-
req,
|
|
362
|
-
undefined,
|
|
363
|
-
202,
|
|
364
|
-
);
|
|
365
|
-
const body = (await res.json()) as AcceptedResponse;
|
|
366
|
-
const terminal = await waitForJobTerminal(body.job_id);
|
|
367
|
-
expect(terminal.status).toBe("complete");
|
|
368
|
-
|
|
369
|
-
expect(gatewayDebugExportMock).toHaveBeenCalledTimes(1);
|
|
370
|
-
const validation = validateVBundle(new Uint8Array(capturedBody!));
|
|
371
|
-
expect(validation.is_valid).toBe(true);
|
|
372
|
-
const manifest = validation.manifest!;
|
|
373
|
-
expect(manifest.export_options.include_gateway).toBe(true);
|
|
374
|
-
const gatewayEntry = manifest.contents.find(
|
|
375
|
-
(f) => f.path === "gateway/export.tar.gz",
|
|
376
|
-
);
|
|
377
|
-
expect(gatewayEntry?.size_bytes).toBe(FAKE_GATEWAY_ARCHIVE.length);
|
|
378
|
-
// Staff never receive credentials. The store mock reports itself
|
|
379
|
-
// unreachable, which would force `secrets_redacted: false` had the
|
|
380
|
-
// handler tried to collect them.
|
|
381
|
-
expect(manifest.secrets_redacted).toBe(true);
|
|
382
|
-
expect(
|
|
383
|
-
manifest.contents.some((f) => f.path.startsWith("credentials/")),
|
|
384
|
-
).toBe(false);
|
|
385
|
-
} finally {
|
|
386
|
-
await fixture.close();
|
|
387
|
-
}
|
|
388
|
-
}, 15_000);
|
|
389
|
-
|
|
390
|
-
test("the default profile never asks the gateway", async () => {
|
|
391
|
-
gatewayDebugExportMock.mockClear();
|
|
392
|
-
let capturedBody: Buffer | undefined;
|
|
393
|
-
const fixture = await startFixtureServer(async (req, res) => {
|
|
394
|
-
capturedBody = await collectBody(req);
|
|
395
|
-
res.writeHead(200);
|
|
396
|
-
res.end();
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
try {
|
|
400
|
-
const req = new Request("http://localhost/v1/migrations/export-to-gcs", {
|
|
401
|
-
method: "POST",
|
|
402
|
-
headers: { "Content-Type": "application/json" },
|
|
403
|
-
body: JSON.stringify({
|
|
404
|
-
upload_url: makeFakeSignedUploadUrl(fixture.port),
|
|
405
|
-
}),
|
|
406
|
-
});
|
|
407
|
-
const res = await callHandler(
|
|
408
|
-
handleMigrationExportToGcs,
|
|
409
|
-
req,
|
|
410
|
-
undefined,
|
|
411
|
-
202,
|
|
412
|
-
);
|
|
413
|
-
const body = (await res.json()) as AcceptedResponse;
|
|
414
|
-
const terminal = await waitForJobTerminal(body.job_id);
|
|
415
|
-
expect(terminal.status).toBe("complete");
|
|
416
|
-
|
|
417
|
-
expect(gatewayDebugExportMock).not.toHaveBeenCalled();
|
|
418
|
-
const manifest = validateVBundle(new Uint8Array(capturedBody!)).manifest!;
|
|
419
|
-
expect(manifest.export_options.include_gateway).toBeUndefined();
|
|
420
|
-
expect(manifest.contents.some((f) => f.path.startsWith("gateway/"))).toBe(
|
|
421
|
-
false,
|
|
422
|
-
);
|
|
423
|
-
} finally {
|
|
424
|
-
await fixture.close();
|
|
425
|
-
}
|
|
426
|
-
}, 15_000);
|
|
427
|
-
|
|
428
|
-
test("a debug bundle is not sent at all if the gateway cannot export", async () => {
|
|
429
|
-
gatewayDebugExportMock.mockImplementationOnce(async () => {
|
|
430
|
-
throw new Error("gateway socket missing");
|
|
431
|
-
});
|
|
432
|
-
let putCount = 0;
|
|
433
|
-
const fixture = await startFixtureServer(async (_req, res) => {
|
|
434
|
-
putCount += 1;
|
|
435
|
-
res.writeHead(200);
|
|
436
|
-
res.end();
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
const req = new Request("http://localhost/v1/migrations/export-to-gcs", {
|
|
441
|
-
method: "POST",
|
|
442
|
-
headers: { "Content-Type": "application/json" },
|
|
443
|
-
body: JSON.stringify({
|
|
444
|
-
upload_url: makeFakeSignedUploadUrl(fixture.port),
|
|
445
|
-
profile: "debug",
|
|
446
|
-
}),
|
|
447
|
-
});
|
|
448
|
-
const res = await callHandler(
|
|
449
|
-
handleMigrationExportToGcs,
|
|
450
|
-
req,
|
|
451
|
-
undefined,
|
|
452
|
-
202,
|
|
453
|
-
);
|
|
454
|
-
const body = (await res.json()) as { job_id: string };
|
|
455
|
-
const terminal = await waitForJobTerminal(body.job_id);
|
|
456
|
-
expect(terminal.status).toBe("failed");
|
|
457
|
-
expect(terminal.error?.code).toBe("gateway_debug_export_failed");
|
|
458
|
-
expect(putCount).toBe(0);
|
|
459
|
-
} finally {
|
|
460
|
-
await fixture.close();
|
|
461
|
-
}
|
|
462
|
-
}, 15_000);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
describe("handleMigrationExportToGcs — concurrency", () => {
|
|
466
|
-
test("second export while first is running returns 409 export_in_progress", async () => {
|
|
467
|
-
// First fixture: deliberately slow so the first job stays pending long
|
|
468
|
-
// enough for the second request to collide with it.
|
|
469
|
-
let release: () => void = () => {};
|
|
470
|
-
const released = new Promise<void>((resolve) => {
|
|
471
|
-
release = resolve;
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
const slow = await startFixtureServer(async (req, res) => {
|
|
475
|
-
if (req.method !== "PUT") {
|
|
476
|
-
res.writeHead(405);
|
|
477
|
-
res.end();
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
// Consume the body but defer the response until the test says so.
|
|
481
|
-
await collectBody(req);
|
|
482
|
-
await released;
|
|
483
|
-
res.writeHead(200);
|
|
484
|
-
res.end();
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
// Second fixture: never hit in this test — but we still need a valid
|
|
488
|
-
// URL shape so the handler gets past URL validation on the second
|
|
489
|
-
// call. We point the second request at the same fixture URL; the
|
|
490
|
-
// handler rejects before the URL is fetched.
|
|
491
|
-
try {
|
|
492
|
-
const firstReq = new Request(
|
|
493
|
-
"http://localhost/v1/migrations/export-to-gcs",
|
|
494
|
-
{
|
|
495
|
-
method: "POST",
|
|
496
|
-
headers: { "Content-Type": "application/json" },
|
|
497
|
-
body: JSON.stringify({
|
|
498
|
-
upload_url: makeFakeSignedUploadUrl(slow.port),
|
|
499
|
-
}),
|
|
500
|
-
},
|
|
501
|
-
);
|
|
502
|
-
const firstRes = await callHandler(
|
|
503
|
-
handleMigrationExportToGcs,
|
|
504
|
-
firstReq,
|
|
505
|
-
undefined,
|
|
506
|
-
202,
|
|
507
|
-
);
|
|
508
|
-
expect(firstRes.status).toBe(202);
|
|
509
|
-
const firstBody = (await firstRes.json()) as AcceptedResponse;
|
|
510
|
-
|
|
511
|
-
// Yield to the microtask queue so the first job's runner actually
|
|
512
|
-
// transitions from pending → running before we try to start a
|
|
513
|
-
// second one.
|
|
514
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
515
|
-
|
|
516
|
-
const secondReq = new Request(
|
|
517
|
-
"http://localhost/v1/migrations/export-to-gcs",
|
|
518
|
-
{
|
|
519
|
-
method: "POST",
|
|
520
|
-
headers: { "Content-Type": "application/json" },
|
|
521
|
-
body: JSON.stringify({
|
|
522
|
-
upload_url: makeFakeSignedUploadUrl(slow.port),
|
|
523
|
-
}),
|
|
524
|
-
},
|
|
525
|
-
);
|
|
526
|
-
const secondRes = await callHandler(
|
|
527
|
-
handleMigrationExportToGcs,
|
|
528
|
-
secondReq,
|
|
529
|
-
undefined,
|
|
530
|
-
202,
|
|
531
|
-
);
|
|
532
|
-
expect(secondRes.status).toBe(409);
|
|
533
|
-
const secondBody = (await secondRes.json()) as ErrorEnvelope;
|
|
534
|
-
expect(secondBody.error.code).toBe("export_in_progress");
|
|
535
|
-
expect(secondBody.error.message).toContain(firstBody.job_id);
|
|
536
|
-
|
|
537
|
-
// Unblock the first fixture so the first job can finalize and the
|
|
538
|
-
// afterEach cleanup sweeps it out.
|
|
539
|
-
release();
|
|
540
|
-
await waitForJobTerminal(firstBody.job_id);
|
|
541
|
-
} finally {
|
|
542
|
-
release();
|
|
543
|
-
await slow.close();
|
|
544
|
-
}
|
|
545
|
-
}, 20_000);
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
describe("handleMigrationExportToGcs — upload failure", () => {
|
|
549
|
-
test("PUT server responds 500 → job failed with upload_failed + upstreamStatus", async () => {
|
|
550
|
-
const fixture = await startFixtureServer(async (req, res) => {
|
|
551
|
-
// Drain the body so the client side completes its write before we
|
|
552
|
-
// send the 500 response. Without this, fetch may surface the 500 as
|
|
553
|
-
// a transport-level error instead of a non-2xx response.
|
|
554
|
-
await collectBody(req);
|
|
555
|
-
res.writeHead(500);
|
|
556
|
-
res.end("upload rejected");
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
try {
|
|
560
|
-
const req = new Request("http://localhost/v1/migrations/export-to-gcs", {
|
|
561
|
-
method: "POST",
|
|
562
|
-
headers: { "Content-Type": "application/json" },
|
|
563
|
-
body: JSON.stringify({
|
|
564
|
-
upload_url: makeFakeSignedUploadUrl(fixture.port),
|
|
565
|
-
}),
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
const res = await callHandler(
|
|
569
|
-
handleMigrationExportToGcs,
|
|
570
|
-
req,
|
|
571
|
-
undefined,
|
|
572
|
-
202,
|
|
573
|
-
);
|
|
574
|
-
expect(res.status).toBe(202);
|
|
575
|
-
const body = (await res.json()) as AcceptedResponse;
|
|
576
|
-
|
|
577
|
-
const terminal = await waitForJobTerminal(body.job_id);
|
|
578
|
-
expect(terminal.status).toBe("failed");
|
|
579
|
-
expect(terminal.error).toBeDefined();
|
|
580
|
-
expect(terminal.error?.code).toBe("upload_failed");
|
|
581
|
-
expect(terminal.error?.upstreamStatus).toBe(500);
|
|
582
|
-
} finally {
|
|
583
|
-
await fixture.close();
|
|
584
|
-
}
|
|
585
|
-
}, 15_000);
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
describe("handleMigrationExportToGcs — redirect handling", () => {
|
|
589
|
-
test("PUT server responds with a 302 redirect → job failed (no SSRF follow)", async () => {
|
|
590
|
-
// If the upstream "storage.googleapis.com" responded with a 3xx to an
|
|
591
|
-
// attacker-controlled host, default fetch would follow and PUT bytes
|
|
592
|
-
// there. `redirect: "error"` on the fetch must surface the redirect
|
|
593
|
-
// as a transport failure instead — the job must end `failed` rather
|
|
594
|
-
// than `complete`, and no bytes must reach the redirect target.
|
|
595
|
-
let redirectTargetHit = false;
|
|
596
|
-
const redirectTarget = await startFixtureServer(async (_req, res) => {
|
|
597
|
-
redirectTargetHit = true;
|
|
598
|
-
res.writeHead(200);
|
|
599
|
-
res.end();
|
|
600
|
-
});
|
|
601
|
-
const fixture = await startFixtureServer(async (_req, res) => {
|
|
602
|
-
res.writeHead(302, {
|
|
603
|
-
Location: `http://127.0.0.1:${redirectTarget.port}/attacker`,
|
|
604
|
-
});
|
|
605
|
-
res.end();
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
try {
|
|
609
|
-
const req = new Request("http://localhost/v1/migrations/export-to-gcs", {
|
|
610
|
-
method: "POST",
|
|
611
|
-
headers: { "Content-Type": "application/json" },
|
|
612
|
-
body: JSON.stringify({
|
|
613
|
-
upload_url: makeFakeSignedUploadUrl(fixture.port),
|
|
614
|
-
}),
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
const res = await callHandler(
|
|
618
|
-
handleMigrationExportToGcs,
|
|
619
|
-
req,
|
|
620
|
-
undefined,
|
|
621
|
-
202,
|
|
622
|
-
);
|
|
623
|
-
expect(res.status).toBe(202);
|
|
624
|
-
const body = (await res.json()) as AcceptedResponse;
|
|
625
|
-
|
|
626
|
-
const terminal = await waitForJobTerminal(body.job_id);
|
|
627
|
-
expect(terminal.status).toBe("failed");
|
|
628
|
-
expect(redirectTargetHit).toBe(false);
|
|
629
|
-
} finally {
|
|
630
|
-
await fixture.close();
|
|
631
|
-
await redirectTarget.close();
|
|
632
|
-
}
|
|
633
|
-
}, 15_000);
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
describe("handleMigrationExportToGcs — URL validation", () => {
|
|
637
|
-
test("rejects non-https scheme when allowlist is default (400)", async () => {
|
|
638
|
-
// Temporarily reset the validator to production defaults so this
|
|
639
|
-
// assertion exercises the strict scheme check.
|
|
640
|
-
_setUrlImportValidatorOptionsForTests(undefined);
|
|
641
|
-
try {
|
|
642
|
-
const req = new Request("http://localhost/v1/migrations/export-to-gcs", {
|
|
643
|
-
method: "POST",
|
|
644
|
-
headers: { "Content-Type": "application/json" },
|
|
645
|
-
body: JSON.stringify({
|
|
646
|
-
upload_url: "http://storage.googleapis.com/b/o?X-Goog-Signature=fake",
|
|
647
|
-
}),
|
|
648
|
-
});
|
|
649
|
-
const res = await callHandler(
|
|
650
|
-
handleMigrationExportToGcs,
|
|
651
|
-
req,
|
|
652
|
-
undefined,
|
|
653
|
-
202,
|
|
654
|
-
);
|
|
655
|
-
expect(res.status).toBe(400);
|
|
656
|
-
const body = (await res.json()) as ErrorEnvelope;
|
|
657
|
-
expect(body.error.code).toBe("invalid_upload_url");
|
|
658
|
-
expect(body.error.message).toContain("scheme");
|
|
659
|
-
} finally {
|
|
660
|
-
_setUrlImportValidatorOptionsForTests({
|
|
661
|
-
allowedHosts: ["127.0.0.1", "storage.googleapis.com"],
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
test("rejects wrong host (400)", async () => {
|
|
667
|
-
const req = new Request("http://localhost/v1/migrations/export-to-gcs", {
|
|
668
|
-
method: "POST",
|
|
669
|
-
headers: { "Content-Type": "application/json" },
|
|
670
|
-
body: JSON.stringify({
|
|
671
|
-
upload_url: "https://evil.example.com/bucket/obj?X-Goog-Signature=fake",
|
|
672
|
-
}),
|
|
673
|
-
});
|
|
674
|
-
const res = await callHandler(
|
|
675
|
-
handleMigrationExportToGcs,
|
|
676
|
-
req,
|
|
677
|
-
undefined,
|
|
678
|
-
202,
|
|
679
|
-
);
|
|
680
|
-
expect(res.status).toBe(400);
|
|
681
|
-
const body = (await res.json()) as ErrorEnvelope;
|
|
682
|
-
expect(body.error.code).toBe("invalid_upload_url");
|
|
683
|
-
expect(body.error.message).toContain("host");
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
test("rejects path traversal (400)", async () => {
|
|
687
|
-
const req = new Request("http://localhost/v1/migrations/export-to-gcs", {
|
|
688
|
-
method: "POST",
|
|
689
|
-
headers: { "Content-Type": "application/json" },
|
|
690
|
-
body: JSON.stringify({
|
|
691
|
-
upload_url:
|
|
692
|
-
"https://storage.googleapis.com/bucket/..%2Fother?X-Goog-Signature=fake",
|
|
693
|
-
}),
|
|
694
|
-
});
|
|
695
|
-
const res = await callHandler(
|
|
696
|
-
handleMigrationExportToGcs,
|
|
697
|
-
req,
|
|
698
|
-
undefined,
|
|
699
|
-
202,
|
|
700
|
-
);
|
|
701
|
-
expect(res.status).toBe(400);
|
|
702
|
-
const body = (await res.json()) as ErrorEnvelope;
|
|
703
|
-
expect(body.error.code).toBe("invalid_upload_url");
|
|
704
|
-
expect(body.error.message).toContain("path_traversal");
|
|
705
|
-
});
|
|
706
|
-
});
|