From ac00fb840bb7fb1aa07eb994468b3ea2a3f3f982 Mon Sep 17 00:00:00 2001 From: Altay Date: Sat, 29 Aug 2026 10:46:39 +0300 Subject: [PATCH 1/4] test: deepen safe file and transfer live fixtures --- docs/TESTING.md | 34 +++++++-- package.json | 4 +- scripts/bootstrap-tokens.ts | 14 ++++ scripts/live-error.spec.ts | 31 ++++++++ scripts/live-error.ts | 43 +++++++++++ scripts/live-token-cache.spec.ts | 38 ++++++++++ scripts/live-token-cache.ts | 33 +++++++++ scripts/test-live-fresh.ts | 99 ------------------------- scripts/test-live-targets.ts | 44 +++++++++++ test/live/domains/events.ts | 36 +-------- test/live/domains/file-tasks.ts | 85 +++++++++++++-------- test/live/domains/transfers.ts | 107 ++++++++++++++++----------- test/live/support/binary-fixtures.ts | 91 +++++++++++++++++++++++ test/live/support/secrets.ts | 6 +- 14 files changed, 448 insertions(+), 217 deletions(-) create mode 100644 scripts/live-error.spec.ts create mode 100644 scripts/live-error.ts create mode 100644 scripts/live-token-cache.spec.ts create mode 100644 scripts/live-token-cache.ts delete mode 100644 scripts/test-live-fresh.ts create mode 100644 scripts/test-live-targets.ts create mode 100644 test/live/support/binary-fixtures.ts diff --git a/docs/TESTING.md b/docs/TESTING.md index 9d1f648..f85127a 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -168,6 +168,18 @@ safe owned MP4 fixture for media flag, URL, HLS, watch status, and start-from coverage. The shared-friend clone fixture is seeded from the configured secondary account. +Branch-heavy file, event, and transfer checks create only timestamped +`codex_sdk_*` resources. Archive fixtures are uploaded, extracted to a terminal +state, and removed in the same test. Torrent fixtures use a unique unreachable +`example.invalid` tracker so the suite can verify decoded torrent transfers and +metainfo bytes before cancelling and cleaning the owned transfer. URL transfer +fixtures cover terminal error and retry transitions separately. + +A successful `events.getTorrent(...)` check remains intentionally unseeded. +Uploaded torrent transfers did not produce a deterministic owned history event +during repeated live probes, so the suite keeps the missing-event typed error +branch without selecting an arbitrary existing history event. + Use `pnpm secrets:setup` to validate the maintainer-provided SOPS ciphertext and render shared live variables into `.env.local`. The live harness also accepts legacy local aliases when they are already exported in the shell. @@ -188,23 +200,29 @@ Single target: vp pack && vp test run --config vitest.live.config.ts test/live/auth.test.ts ``` -Run explicit targets with fresh runtime tokens: +Run explicit targets with the provisioned runtime tokens: ```bash -pnpm test:live:fresh -- test/live/account.test.ts test/live/tunnel.test.ts +pnpm test:live:targets -- test/live/account.test.ts test/live/tunnel.test.ts ``` -`test:live:fresh` uses the existing credential fixture to mint runtime tokens, -runs only the named test files, and revokes the fresh first-party session before -exiting, including when a test fails. It never writes the runtime tokens to an -env file. +`test:live:targets` runs only the named test files with the existing +`PUTIO_TOKEN_FIRST_PARTY` and `PUTIO_TOKEN_THIRD_PARTY` fixtures. Live-test +execution never calls the password-login endpoint. Refreshing tokens remains a +separate, deliberate bootstrap operation. + +`pnpm bootstrap:tokens` performs that bootstrap once and writes the resulting +tokens to the ignored, owner-readable `.env.live-tokens` cache. Live commands +load that cache before `.env.local`, so routine runs reuse the same sessions. +If the cached sessions expire, run `pnpm bootstrap:tokens -- --refresh` to +replace them deliberately; bootstrap refuses to replace the cache otherwise. An unattended runner with a scoped age identity can run a command without materializing secrets: ```bash sops exec-env --same-process "$PUTIO_SDK_TYPESCRIPT_SOPS_FILE" \ - 'pnpm test:live:fresh -- test/live/account.test.ts test/live/tunnel.test.ts' + 'pnpm test:live:targets -- test/live/account.test.ts test/live/tunnel.test.ts' ``` Run `pnpm secrets:setup` once per worktree with @@ -215,7 +233,7 @@ highest priority. ```bash pnpm secrets:setup # one-time per worktree -pnpm bootstrap:tokens # mints fresh tokens +pnpm bootstrap:tokens # mints and caches tokens once pnpm bootstrap:live-fixtures pnpm test:live # runs the broader live suite against pre-existing tokens pnpm secrets:clean # before `git worktree remove` diff --git a/package.json b/package.json index 0207c98..daa3073 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "lint:unused:prod": "vp pack && knip --production --no-gitignore", "prepack": "vp pack", "secrets:setup": "bash ./scripts/secrets-setup.sh", - "secrets:clean": "rm -f .env.local .env.local.* .env.local.swp", + "secrets:clean": "rm -f .env.live-tokens .env.local .env.local.* .env.local.swp", "test": "vp test --passWithNoTests", "test:compat": "node ./scripts/test-compat-node.ts && node ./scripts/test-compat-browser.ts && node ./scripts/test-compat-bun.ts", "test:compat:browser": "node ./scripts/test-compat-browser.ts", @@ -56,7 +56,7 @@ "test:compat:bun": "node ./scripts/test-compat-bun.ts", "test:compat:node": "node ./scripts/test-compat-node.ts", "test:live": "vp pack && vp test run --config vitest.live.config.ts", - "test:live:fresh": "vp pack && node ./scripts/test-live-fresh.ts", + "test:live:targets": "vp pack && node ./scripts/test-live-targets.ts", "validate:routes": "vp pack && vp run validate:routes:packed", "validate:routes:packed": "node ./scripts/validate-route-matrix.ts", "verify": "vp check . && vp run lint:package && knip && knip --production --no-gitignore && vp run validate:routes:packed && vp test run --coverage --passWithNoTests" diff --git a/scripts/bootstrap-tokens.ts b/scripts/bootstrap-tokens.ts index 2c43062..7936b10 100644 --- a/scripts/bootstrap-tokens.ts +++ b/scripts/bootstrap-tokens.ts @@ -1,8 +1,17 @@ import { spawnSync } from "node:child_process"; +import { hasLiveTokenCache, writeLiveTokenCache } from "./live-token-cache.ts"; import { bootstrapRuntimeTokens } from "../test/live/support/bootstrap.ts"; import { readBootstrapSecrets } from "../test/live/support/secrets.ts"; +const refresh = process.argv.slice(2).includes("--refresh"); + +if ((await hasLiveTokenCache()) && !refresh) { + throw new Error( + "Live tokens are already cached; use test:live or pass --refresh to replace expired tokens", + ); +} + const packageDir = new URL("..", import.meta.url); const buildResult = spawnSync("vp", ["pack"], { @@ -21,6 +30,11 @@ const bootstrapped = await bootstrapRuntimeTokens(secrets, async (config = {}) = createPutioSdkPromiseClient(config), ); +await writeLiveTokenCache({ + firstPartyToken: bootstrapped.firstParty.accessToken, + thirdPartyToken: bootstrapped.thirdParty.accessToken, +}); + console.log( JSON.stringify( { diff --git a/scripts/live-error.spec.ts b/scripts/live-error.spec.ts new file mode 100644 index 0000000..f057b74 --- /dev/null +++ b/scripts/live-error.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { formatLiveError } from "./live-error.ts"; + +describe("formatLiveError", () => { + it("formats normal errors", () => { + expect(formatLiveError(new Error("network unavailable"))).toBe("Error: network unavailable"); + }); + + it("formats tagged SDK errors without relying on their empty message", () => { + const error = Object.assign(new Error(), { + _tag: "PutioAuthError", + body: { + error_type: "TooManyRequests", + }, + retryAfter: "12", + status: 429, + }); + + expect(formatLiveError(error)).toBe("PutioAuthError status=429 TooManyRequests retryAfter=12"); + }); + + it("does not serialize unknown payload fields", () => { + expect( + formatLiveError({ + _tag: "PutioValidationError", + password: "do-not-print", + }), + ).toBe("PutioValidationError"); + }); +}); diff --git a/scripts/live-error.ts b/scripts/live-error.ts new file mode 100644 index 0000000..e24f884 --- /dev/null +++ b/scripts/live-error.ts @@ -0,0 +1,43 @@ +import { Predicate } from "effect"; + +const nonEmptyString = (value: unknown): string | undefined => + Predicate.isString(value) && value.trim().length > 0 ? value.trim() : undefined; + +export const formatLiveError = (error: unknown): string => { + if (error instanceof Error) { + const message = nonEmptyString(error.message); + if (message !== undefined) { + return `${error.name}: ${message}`; + } + } + + if (Predicate.isObject(error)) { + const tag = nonEmptyString(error._tag) ?? nonEmptyString(error.name); + const status = Predicate.isNumber(error.status) ? String(error.status) : undefined; + const body = Predicate.isObject(error.body) ? error.body : undefined; + const errorType = body === undefined ? undefined : nonEmptyString(body.error_type); + const retryAfter = nonEmptyString(error.retryAfter); + const reset = nonEmptyString(error.reset); + const action = nonEmptyString(error.action); + const details = [ + tag, + status === undefined ? undefined : `status=${status}`, + errorType, + retryAfter === undefined ? undefined : `retryAfter=${retryAfter}`, + reset === undefined ? undefined : `reset=${reset}`, + action === undefined ? undefined : `action=${action}`, + ] + .filter(Predicate.isString) + .join(" "); + + if (details.length > 0) { + return details; + } + + if ("cause" in error) { + return formatLiveError(error.cause); + } + } + + return "Unknown live test failure"; +}; diff --git a/scripts/live-token-cache.spec.ts b/scripts/live-token-cache.spec.ts new file mode 100644 index 0000000..08dd7c2 --- /dev/null +++ b/scripts/live-token-cache.spec.ts @@ -0,0 +1,38 @@ +import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +import { afterEach, describe, expect, it } from "vite-plus/test"; + +import { hasLiveTokenCache, writeLiveTokenCache } from "./live-token-cache.ts"; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { force: true, recursive: true })), + ); +}); + +describe("live token cache", () => { + it("writes ignored dotenv tokens with owner-only permissions", async () => { + const directory = await mkdtemp(join(tmpdir(), "putio-sdk-live-tokens-")); + temporaryDirectories.push(directory); + const url = pathToFileURL(join(directory, ".env.live-tokens")); + await writeLiveTokenCache( + { + firstPartyToken: "first-test-token", + thirdPartyToken: "third-test-token", + }, + url, + ); + + expect(await hasLiveTokenCache(url)).toBe(true); + expect(await readFile(url, "utf8")).toBe( + 'PUTIO_TOKEN_FIRST_PARTY="first-test-token"\n' + + 'PUTIO_TOKEN_THIRD_PARTY="third-test-token"\n', + ); + expect((await stat(url)).mode & 0o777).toBe(0o600); + }); +}); diff --git a/scripts/live-token-cache.ts b/scripts/live-token-cache.ts new file mode 100644 index 0000000..0b8653d --- /dev/null +++ b/scripts/live-token-cache.ts @@ -0,0 +1,33 @@ +import { access, chmod, writeFile } from "node:fs/promises"; + +export const liveTokenCacheUrl = new URL("../.env.live-tokens", import.meta.url); + +export const hasLiveTokenCache = async (url: URL = liveTokenCacheUrl): Promise => { + try { + await access(url); + return true; + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") { + return false; + } + + throw error; + } +}; + +export const writeLiveTokenCache = async ( + tokens: { + readonly firstPartyToken: string; + readonly thirdPartyToken: string; + }, + url: URL = liveTokenCacheUrl, +): Promise => { + const contents = [ + `PUTIO_TOKEN_FIRST_PARTY=${JSON.stringify(tokens.firstPartyToken)}`, + `PUTIO_TOKEN_THIRD_PARTY=${JSON.stringify(tokens.thirdPartyToken)}`, + "", + ].join("\n"); + + await writeFile(url, contents, { mode: 0o600 }); + await chmod(url, 0o600); +}; diff --git a/scripts/test-live-fresh.ts b/scripts/test-live-fresh.ts deleted file mode 100644 index 280d1ee..0000000 --- a/scripts/test-live-fresh.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { existsSync } from "node:fs"; -import { relative, resolve, sep } from "node:path"; - -import { - bootstrapFirstPartyToken, - bootstrapThirdPartyToken, -} from "../test/live/support/bootstrap.ts"; -import { readBootstrapSecrets } from "../test/live/support/secrets.ts"; - -const args = process.argv.slice(2); -const targets = args[0] === "--" ? args.slice(1) : args; - -if (targets.length === 0) { - throw new Error("Pass one or more explicit test/live/**/*.test.ts files"); -} - -const liveRoot = resolve("test/live"); - -for (const target of targets) { - const relativeTarget = relative(liveRoot, resolve(target)); - const isLiveTest = - target.startsWith("test/live/") && - target.endsWith(".test.ts") && - relativeTarget !== ".." && - !relativeTarget.startsWith(`..${sep}`); - - if (!isLiveTest || !existsSync(target)) { - throw new Error(`Unsupported live test target: ${target}`); - } -} - -const { createPutioSdkPromiseClient } = await import("../dist/index.js"); -const clients = new Set>(); -const createClient = async (config: Record = {}) => { - const client = createPutioSdkPromiseClient(config); - clients.add(client); - return client; -}; -const secrets = readBootstrapSecrets(); -let firstParty: Awaited> | null = null; -let cleanupError: Error | null = null; -let runError: Error | null = null; -let status = 1; - -try { - firstParty = await bootstrapFirstPartyToken(secrets, createClient); - const thirdParty = await bootstrapThirdPartyToken( - firstParty.accessToken, - secrets.thirdPartyClientId, - createClient, - ); - const result = spawnSync("vp", ["test", "run", "--config", "vitest.live.config.ts", ...targets], { - env: { - ...process.env, - PUTIO_TOKEN_FIRST_PARTY: firstParty.accessToken, - PUTIO_TOKEN_THIRD_PARTY: thirdParty.accessToken, - }, - stdio: "inherit", - }); - - runError = result.error ?? null; - status = result.status ?? 1; -} catch (error) { - runError = error instanceof Error ? error : new Error("Unknown live test failure"); -} finally { - try { - if (firstParty !== null) { - if (firstParty.tokenId === null) { - cleanupError = new Error("Fresh first-party session did not return a token ID"); - } else { - const cleanupClient = await createClient({ accessToken: firstParty.accessToken }); - await cleanupClient.auth.revokeClient(firstParty.tokenId); - } - } - } catch (error) { - cleanupError = error instanceof Error ? error : new Error("Unknown token cleanup failure"); - } - - for (const client of clients) { - try { - await client.dispose(); - } catch (error) { - cleanupError ??= - error instanceof Error ? error : new Error("Unknown client disposal failure"); - } - } -} - -if (runError) { - console.error(`Live test execution failed: ${runError.message}`); -} - -if (cleanupError) { - console.error(`Fresh token cleanup failed: ${cleanupError.message}`); - process.exit(1); -} - -process.exit(status); diff --git a/scripts/test-live-targets.ts b/scripts/test-live-targets.ts new file mode 100644 index 0000000..a29f634 --- /dev/null +++ b/scripts/test-live-targets.ts @@ -0,0 +1,44 @@ +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { relative, resolve, sep } from "node:path"; + +import { formatLiveError } from "./live-error.ts"; +import { readLiveTokens } from "../test/live/support/secrets.ts"; + +const args = process.argv.slice(2); +const targets = args[0] === "--" ? args.slice(1) : args; + +if (targets.length === 0) { + throw new Error("Pass one or more explicit test/live/**/*.test.ts files"); +} + +const liveRoot = resolve("test/live"); + +for (const target of targets) { + const relativeTarget = relative(liveRoot, resolve(target)); + const isLiveTest = + target.startsWith("test/live/") && + target.endsWith(".test.ts") && + relativeTarget !== ".." && + !relativeTarget.startsWith(`..${sep}`); + + if (!isLiveTest || !existsSync(target)) { + throw new Error(`Unsupported live test target: ${target}`); + } +} + +const tokens = readLiveTokens(); +const result = spawnSync("vp", ["test", "run", "--config", "vitest.live.config.ts", ...targets], { + env: { + ...process.env, + PUTIO_TOKEN_FIRST_PARTY: tokens.firstPartyToken, + PUTIO_TOKEN_THIRD_PARTY: tokens.thirdPartyToken, + }, + stdio: "inherit", +}); + +if (result.error) { + console.error(`Live test execution failed: ${formatLiveError(result.error)}`); +} + +process.exit(result.status ?? 1); diff --git a/test/live/domains/events.ts b/test/live/domains/events.ts index fe94d65..7464645 100644 --- a/test/live/domains/events.ts +++ b/test/live/domains/events.ts @@ -1,4 +1,4 @@ -import { assertPresent, createClients, createLiveHarness } from "../support/harness.js"; +import { createClients, createLiveHarness } from "../support/harness.js"; const { authClient, oauthClient } = await createClients({ authClient: "PUTIO_TOKEN_FIRST_PARTY", @@ -6,7 +6,7 @@ const { authClient, oauthClient } = await createClients({ }); const live = createLiveHarness("events live"); -const { assert, assertOperationError, finish, run, sleep } = live; +const { assert, assertErrorTag, assertOperationError, finish, run, sleep } = live; void assertOperationError; void sleep; @@ -96,12 +96,7 @@ await run("events list invalid per_page yields typed error", async () => { }); throw new Error("expected invalid per_page to fail"); } catch (error) { - return assertOperationError(error, { - domain: "events", - errorType: "INVALID_PER_PAGE", - operation: "list", - statusCode: 400, - }); + return assertErrorTag(error, { tag: "PutioValidationError" }); } }); @@ -152,29 +147,4 @@ await run("events torrent missing id yields typed error", async () => { } }); -await run("events torrent for non-upload event currently yields 404", async () => { - const result = await oauthClient.events.list({ - per_page: 20, - }); - const nonUpload = assertPresent( - result.events.find((event) => event.type !== "upload"), - "expected at least one non-upload event for torrent probe", - ); - - try { - await oauthClient.events.getTorrent(nonUpload.id); - throw new Error("expected non-upload torrent lookup to fail"); - } catch (error) { - return { - event_type: nonUpload.type, - id: nonUpload.id, - ...assertOperationError(error, { - domain: "events", - operation: "getTorrent", - statusCode: 404, - }), - }; - } -}); - finish(); diff --git a/test/live/domains/file-tasks.ts b/test/live/domains/file-tasks.ts index e5868e7..7c8a08c 100644 --- a/test/live/domains/file-tasks.ts +++ b/test/live/domains/file-tasks.ts @@ -4,6 +4,7 @@ import { createLiveHarness, isFileUploadFileResult, } from "../support/harness.js"; +import { createStoredZipFile } from "../support/binary-fixtures.ts"; import { requireOwnedVideoFixture } from "../support/media.ts"; const { authClient, client } = await createClients({ @@ -17,15 +18,18 @@ const { assert, assertOperationError, finish, run, sleep } = live; void assertOperationError; void sleep; -const getArchiveCandidate = async () => { - const search = await client.files.search({ - per_page: 20, - query: "zip", - }); +const waitForExtractionTerminalState = async (id: number) => { + for (let attempt = 0; attempt < 30; attempt += 1) { + const extraction = (await client.files.listExtractions()).find((item) => item.id === id); + + if (extraction?.status === "EXTRACTED" || extraction?.status === "ERROR") { + return extraction; + } + + await sleep(1_000); + } - return ( - search.files.find((file) => file.file_type === "ARCHIVE" && file.is_shared === false) ?? null - ); + throw new Error(`timed out waiting for extraction ${id} to reach a terminal state`); }; await run("files list extractions shape", async () => { @@ -107,16 +111,17 @@ await run("files start_from roundtrip semantics", async () => { await run("files next-file natural ordering with disposable fixtures", async () => { const created: Array<{ readonly id: number; readonly name: string }> = []; + const suffix = Date.now(); const folder = await authClient.files.createFolder({ - name: `codex_sdk_next_file_${Date.now()}`, + name: `codex_sdk_next_file_${suffix}`, parent_id: 0, }); try { for (const name of [ - `codex_sdk_episode_1_${Date.now()}.txt`, - `codex_sdk_episode_10_${Date.now()}.txt`, - `codex_sdk_episode_2_${Date.now()}.txt`, + `codex_sdk_episode_1_${suffix}.txt`, + `codex_sdk_episode_10_${suffix}.txt`, + `codex_sdk_episode_2_${suffix}.txt`, ]) { const upload = await authClient.files.upload({ file: new File(["sdk next-file probe\n"], name, { @@ -141,7 +146,12 @@ await run("files next-file natural ordering with disposable fixtures", async () "expected episode 2 fixture", ); - const next = await client.files.findNext(episode1.id, "FILE"); + let next = await client.files.findNext(episode1.id, "FILE"); + + for (let attempt = 0; next.id !== episode2.id && attempt < 10; attempt += 1) { + await sleep(500); + next = await client.files.findNext(episode1.id, "FILE"); + } assert(next.id === episode2.id, "expected natural ordering to skip episode 10"); @@ -157,31 +167,44 @@ await run("files next-file natural ordering with disposable fixtures", async () } }); -await run("files extract and cleanup", async () => { - const archive = await getArchiveCandidate(); +await run("files owned archive extraction reaches terminal success", async () => { + const name = `codex_sdk_extract_${Date.now()}.zip`; + const upload = await authClient.files.upload({ + file: createStoredZipFile(name), + fileName: name, + parentId: 0, + }); - if (!archive) { - throw new Error("expected owned archive candidate"); + if (!isFileUploadFileResult(upload)) { + throw new Error("expected archive upload to return a file"); } - const created = await client.files.extract({ - ids: [archive.id], - }); + const extractionIds: number[] = []; + const extractedFileIds: number[] = []; - assert(Array.isArray(created), "expected extraction result array"); + try { + const created = await client.files.extract({ ids: [upload.file.id] }); + assert(created.length === 1, "expected one extraction task"); + const extraction = assertPresent(created[0], "expected extraction task"); + extractionIds.push(extraction.id); - const listed = await client.files.listExtractions(); - const createdIds = created.map((item) => item.id); + const terminal = await waitForExtractionTerminalState(extraction.id); + assert(terminal.status === "EXTRACTED", "expected extraction to succeed"); + extractedFileIds.push(...terminal.files); - for (const extractionId of createdIds) { - await client.files.deleteExtraction(extractionId); + return { + archive_id: upload.file.id, + extracted_file_count: terminal.files.length, + final_status: terminal.status, + }; + } finally { + for (const extractionId of extractionIds) { + await client.files.deleteExtraction(extractionId).catch(() => undefined); + } + await authClient.files + .delete([upload.file.id, ...extractedFileIds], { skipTrash: true }) + .catch(() => undefined); } - - return { - archive_id: archive.id, - created_count: created.length, - listed_match_count: listed.filter((item) => createdIds.includes(item.id)).length, - }; }); await run("files mp4 status on folder yields typed not-file", async () => { diff --git a/test/live/domains/transfers.ts b/test/live/domains/transfers.ts index f8e4745..3bb20b7 100644 --- a/test/live/domains/transfers.ts +++ b/test/live/domains/transfers.ts @@ -1,17 +1,17 @@ import { createClients, createLiveHarness } from "../support/harness.js"; +import { createTorrentFile } from "../support/binary-fixtures.ts"; -const { client } = await createClients({ +const { authClient, client } = await createClients({ + authClient: "PUTIO_TOKEN_FIRST_PARTY", client: "PUTIO_TOKEN_THIRD_PARTY", }); const live = createLiveHarness("transfers live"); -const { assert, assertOperationError, finish, run, sleep } = live; +const { assert, assertErrorTag, assertOperationError, finish, run, sleep } = live; void assertOperationError; void sleep; -const SLOW_TRANSFER_PROBE_URL = "https://speed.hetzner.de/100MB.bin"; - const waitForTransferError = async (id: number) => { for (let attempt = 0; attempt < 8; attempt += 1) { const transfer = await client.transfers.get(id); @@ -26,6 +26,20 @@ const waitForTransferError = async (id: number) => { throw new Error("timed out waiting for transfer to reach ERROR"); }; +const waitForTorrentTransfer = async (id: number) => { + for (let attempt = 0; attempt < 20; attempt += 1) { + const transfer = await authClient.transfers.get(id); + + if (transfer.type === "TORRENT") { + return transfer; + } + + await sleep(500); + } + + throw new Error("timed out waiting for uploaded transfer to decode as TORRENT"); +}; + await run("transfers list shape", async () => { const result = await client.transfers.list({ per_page: 5, @@ -78,7 +92,7 @@ await run("transfers addMany envelope shape", async () => { url: `https://example.invalid/codex-transfer-${Date.now()}.iso`, }, { - url: "", + url: "not-a-url", }, ]); @@ -86,18 +100,18 @@ await run("transfers addMany envelope shape", async () => { assert(Array.isArray(result.errors), "expected per-item errors array"); assert(result.transfers.length >= 1, "expected at least one created transfer"); - const firstTransferId = result.transfers[0]?.id; const firstError = result.errors[0]; - if (typeof firstTransferId === "number") { - await client.transfers.cancel([firstTransferId]).catch(() => undefined); - await client.transfers.clean([firstTransferId]).catch(() => undefined); + for (const transfer of result.transfers) { + await client.transfers.cancel([transfer.id]).catch(() => undefined); + await client.transfers.remove({ ids: [transfer.id] }).catch(() => undefined); + await client.transfers.clean([transfer.id]).catch(() => undefined); } return { behaves_like_partial: result.errors.length > 0, first_error_type: firstError?.error_type ?? null, - first_transfer_id: firstTransferId ?? null, + first_transfer_id: result.transfers[0]?.id ?? null, transfer_count: result.transfers.length, error_count: result.errors.length, }; @@ -110,12 +124,7 @@ await run("empty transfer url yields typed error", async () => { }); throw new Error("expected empty transfer url to fail"); } catch (error) { - return assertOperationError(error, { - domain: "transfers", - errorType: "EMPTY_URL", - operation: "add", - statusCode: 400, - }); + return assertErrorTag(error, { tag: "PutioValidationError" }); } }); @@ -209,33 +218,6 @@ await run("non-torrent reannounce currently falls back to typed bad request", as } }); -await run("retry on fresh non-error transfer returns transfer shape", async () => { - const created = await client.transfers.add({ - url: SLOW_TRANSFER_PROBE_URL, - }); - - try { - const current = await client.transfers.get(created.id); - - if (current.status === "ERROR") { - throw new Error("fresh transfer reached ERROR before the non-error retry branch was checked"); - } - - const retried = await client.transfers.retry(created.id); - - assert(retried.id === created.id, "expected retry to return same transfer"); - assert(retried.status !== "ERROR", "expected retried transfer to remain non-error"); - - return { - initial_status: current.status, - retried_status: retried.status, - }; - } finally { - await client.transfers.cancel([created.id]).catch(() => undefined); - await client.transfers.clean([created.id]).catch(() => undefined); - } -}); - await run("transfers disposable lifecycle", async () => { const countBefore = await client.transfers.count(); @@ -259,6 +241,10 @@ await run("transfers disposable lifecycle", async () => { const errored = await waitForTransferError(created.id); assert(typeof errored.error_message === "string", "expected transfer error message"); + const retried = await client.transfers.retry(created.id); + assert(retried.status !== "ERROR", "expected retry to leave terminal error state"); + const retriedError = await waitForTransferError(created.id); + const fetched = await client.transfers.get(created.id); assert(fetched.id === created.id, "expected get to return created transfer"); @@ -271,6 +257,8 @@ await run("transfers disposable lifecycle", async () => { count_before: countBefore, created_id: created.id, final_status: errored.status, + retried_status: retried.status, + retried_terminal_status: retriedError.status, }; } finally { await client.transfers.cancel([created.id]).catch(() => undefined); @@ -278,6 +266,39 @@ await run("transfers disposable lifecycle", async () => { } }); +await run("uploaded torrent exposes decoded transfer and metainfo bytes", async () => { + const name = `codex_sdk_torrent_transfer_${Date.now()}`; + const upload = await authClient.files.upload({ + file: createTorrentFile(name), + fileName: `${name}.torrent`, + parentId: 0, + }); + + if (upload.type !== "transfer") { + throw new Error("expected torrent upload to return a transfer"); + } + + const transferId = upload.transfer.id; + + try { + const transfer = await waitForTorrentTransfer(transferId); + const torrent = await authClient.transfers.getTorrent(transferId); + assert(torrent.byteLength > 20, "expected torrent metainfo bytes"); + assert(torrent[0] === 0x64, "expected bencoded torrent metainfo"); + + return { + status: transfer.status, + torrent_bytes: torrent.byteLength, + transfer_id: transferId, + type: transfer.type, + }; + } finally { + await authClient.transfers.cancel([transferId]).catch(() => undefined); + await authClient.transfers.remove({ ids: [transferId] }).catch(() => undefined); + await authClient.transfers.clean([transferId]).catch(() => undefined); + } +}); + await run("transfers clean returns deleted ids array", async () => { const result = await client.transfers.clean(); assert(Array.isArray(result.deleted_ids), "expected deleted_ids array"); diff --git a/test/live/support/binary-fixtures.ts b/test/live/support/binary-fixtures.ts new file mode 100644 index 0000000..8b3607a --- /dev/null +++ b/test/live/support/binary-fixtures.ts @@ -0,0 +1,91 @@ +import { createHash } from "node:crypto"; + +const encoder = new TextEncoder(); + +const concatBytes = (...parts: ReadonlyArray): Uint8Array => { + const result = new Uint8Array(parts.reduce((length, part) => length + part.byteLength, 0)); + let offset = 0; + + for (const part of parts) { + result.set(part, offset); + offset += part.byteLength; + } + + return result; +}; + +const encodeText = (value: string): Uint8Array => encoder.encode(value); + +const crc32 = (value: Uint8Array): number => { + let crc = 0xffffffff; + + for (const byte of value) { + crc ^= byte; + for (let bit = 0; bit < 8; bit += 1) { + crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); + } + } + + return (crc ^ 0xffffffff) >>> 0; +}; + +const zipHeader = (length: number, write: (view: DataView) => void): Uint8Array => { + const bytes = new Uint8Array(length); + write(new DataView(bytes.buffer)); + return bytes; +}; + +export const createStoredZipFile = (name: string): File => { + const entryName = encodeText("fixture.txt"); + const contents = encodeText("put.io SDK live archive fixture\n"); + const checksum = crc32(contents); + const localHeader = zipHeader(30, (view) => { + view.setUint32(0, 0x04034b50, true); + view.setUint16(4, 20, true); + view.setUint32(14, checksum, true); + view.setUint32(18, contents.byteLength, true); + view.setUint32(22, contents.byteLength, true); + view.setUint16(26, entryName.byteLength, true); + }); + const centralHeader = zipHeader(46, (view) => { + view.setUint32(0, 0x02014b50, true); + view.setUint16(4, 20, true); + view.setUint16(6, 20, true); + view.setUint32(16, checksum, true); + view.setUint32(20, contents.byteLength, true); + view.setUint32(24, contents.byteLength, true); + view.setUint16(28, entryName.byteLength, true); + }); + const centralOffset = localHeader.byteLength + entryName.byteLength + contents.byteLength; + const centralSize = centralHeader.byteLength + entryName.byteLength; + const end = zipHeader(22, (view) => { + view.setUint32(0, 0x06054b50, true); + view.setUint16(8, 1, true); + view.setUint16(10, 1, true); + view.setUint32(12, centralSize, true); + view.setUint32(16, centralOffset, true); + }); + const archive = concatBytes(localHeader, entryName, contents, centralHeader, entryName, end); + + return new File([archive], name, { type: "application/zip" }); +}; + +export const createTorrentFile = (name: string): File => { + const payload = encodeText("put.io SDK live torrent fixture\n"); + const pieceHash = new Uint8Array(createHash("sha1").update(payload).digest()); + const payloadName = `${name}.txt`; + const info = concatBytes( + encodeText(`d6:lengthi${payload.byteLength}e4:name${payloadName.length}:${payloadName}`), + encodeText(`12:piece lengthi16384e6:pieces20:`), + pieceHash, + encodeText("e"), + ); + const announce = `https://example.invalid/${name}`; + const torrent = concatBytes( + encodeText(`d8:announce${announce.length}:${announce}4:info`), + info, + encodeText("e"), + ); + + return new File([torrent], `${name}.torrent`, { type: "application/x-bittorrent" }); +}; diff --git a/test/live/support/secrets.ts b/test/live/support/secrets.ts index 8aa4273..90395b8 100644 --- a/test/live/support/secrets.ts +++ b/test/live/support/secrets.ts @@ -65,7 +65,11 @@ const loadPackageEnvFile = (): void => { packageEnvLoaded = true; const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "../../.."); - loadEnvFiles([join(packageRoot, ".env.local"), join(packageRoot, ".env")]); + loadEnvFiles([ + join(packageRoot, ".env.live-tokens"), + join(packageRoot, ".env.local"), + join(packageRoot, ".env"), + ]); }; export const requireSecret = (key: TKey): string => { From 47d34aa5d909f58855b1b64761fd6c7090e67960 Mon Sep 17 00:00:00 2001 From: Altay Date: Sat, 29 Aug 2026 10:59:58 +0300 Subject: [PATCH 2/4] test: standardize live fixture identity --- docs/TESTING.md | 45 ++++++++----------- scripts/bootstrap-live-fixtures.ts | 2 +- scripts/bootstrap-tokens.ts | 7 +-- scripts/live-error.spec.ts | 31 ------------- scripts/live-error.ts | 43 ------------------ scripts/live-token-cache.spec.ts | 39 +++++++--------- scripts/live-token-cache.ts | 17 +------ scripts/test-live-targets.ts | 3 +- src/domains/auth.spec.ts | 8 ++-- test/live/account.test.ts | 2 +- test/live/domains/config.ts | 2 +- test/live/domains/download-links.ts | 12 +++-- test/live/domains/events.ts | 5 +-- test/live/domains/family.ts | 8 ++-- test/live/domains/file-direct.ts | 6 +-- test/live/domains/file-tasks.ts | 51 +++++---------------- test/live/domains/files.ts | 12 ++--- test/live/domains/ifttt.ts | 8 ++-- test/live/domains/payment.ts | 18 ++++---- test/live/domains/rss.ts | 10 ++--- test/live/domains/sharing.ts | 18 ++++---- test/live/domains/transfers.ts | 70 +++++++++-------------------- test/live/domains/trash.ts | 10 ++--- test/live/domains/zips.ts | 2 +- test/live/oauth.test.ts | 10 ++--- test/live/support/bootstrap.ts | 6 +-- test/live/support/friends.ts | 4 +- test/live/support/media.ts | 4 +- 28 files changed, 148 insertions(+), 305 deletions(-) delete mode 100644 scripts/live-error.spec.ts delete mode 100644 scripts/live-error.ts diff --git a/docs/TESTING.md b/docs/TESTING.md index f85127a..c8fb078 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -151,10 +151,9 @@ Optional direct runtime variables: `PUTIO_LIVE_OWNED_VIDEO_FILE_ID` can pin media live tests to an explicit safe, owned, unshared MP4 fixture. If it is unset, the live harness only accepts -owned MP4s with SDK/example fixture names such as `codex_sdk_*`, -`codex-sdk-*`, `Mario1_507_512kb.mp4`, `Sintel.mp4`, or -`Big Buck Bunny.mp4`; it never selects an arbitrary private video from the -account. +owned MP4s with SDK/example fixture names such as `putio-typescript-sdk-*`, +`Mario1_507_512kb.mp4`, `Sintel.mp4`, or `Big Buck Bunny.mp4`; it never selects +an arbitrary private video from the account. `PUTIO_LIVE_RSS_SOURCE_URL` must point at a known-good RSS feed when running the `rss` target. `PUTIO_TOKEN_PAYMENT_OWNER` must belong to a prepaid owner account @@ -168,17 +167,14 @@ safe owned MP4 fixture for media flag, URL, HLS, watch status, and start-from coverage. The shared-friend clone fixture is seeded from the configured secondary account. -Branch-heavy file, event, and transfer checks create only timestamped -`codex_sdk_*` resources. Archive fixtures are uploaded, extracted to a terminal -state, and removed in the same test. Torrent fixtures use a unique unreachable -`example.invalid` tracker so the suite can verify decoded torrent transfers and -metainfo bytes before cancelling and cleaning the owned transfer. URL transfer -fixtures cover terminal error and retry transitions separately. +File and transfer tests create timestamped `putio-typescript-sdk-*` resources. +Each test removes its archive, extracted files, or transfer before it exits. +Torrent fixtures use an unreachable `example.invalid` tracker. URL fixtures +cover the terminal error and retry states. -A successful `events.getTorrent(...)` check remains intentionally unseeded. -Uploaded torrent transfers did not produce a deterministic owned history event -during repeated live probes, so the suite keeps the missing-event typed error -branch without selecting an arbitrary existing history event. +Uploaded torrents did not create a predictable history event. The suite tests +the missing-event result from `events.getTorrent(...)` and leaves existing +account history alone. Use `pnpm secrets:setup` to validate the maintainer-provided SOPS ciphertext and render shared live variables into `.env.local`. The live harness also accepts @@ -206,16 +202,13 @@ Run explicit targets with the provisioned runtime tokens: pnpm test:live:targets -- test/live/account.test.ts test/live/tunnel.test.ts ``` -`test:live:targets` runs only the named test files with the existing -`PUTIO_TOKEN_FIRST_PARTY` and `PUTIO_TOKEN_THIRD_PARTY` fixtures. Live-test -execution never calls the password-login endpoint. Refreshing tokens remains a -separate, deliberate bootstrap operation. +`test:live:targets` runs only the named files. It reads +`PUTIO_TOKEN_FIRST_PARTY` and `PUTIO_TOKEN_THIRD_PARTY` and never calls password +login. -`pnpm bootstrap:tokens` performs that bootstrap once and writes the resulting -tokens to the ignored, owner-readable `.env.live-tokens` cache. Live commands -load that cache before `.env.local`, so routine runs reuse the same sessions. -If the cached sessions expire, run `pnpm bootstrap:tokens -- --refresh` to -replace them deliberately; bootstrap refuses to replace the cache otherwise. +`pnpm bootstrap:tokens` writes new tokens to the ignored `0600` +`.env.live-tokens` cache. Live commands load it before `.env.local`. Bootstrap +refuses to replace the cache unless you pass `--refresh`. An unattended runner with a scoped age identity can run a command without materializing secrets: @@ -227,9 +220,9 @@ sops exec-env --same-process "$PUTIO_SDK_TYPESCRIPT_SOPS_FILE" \ Run `pnpm secrets:setup` once per worktree with `PUTIO_SDK_TYPESCRIPT_SOPS_FILE` pointing to the supplied ciphertext. The -materialized file is `0600` and gitignored. Live commands auto-load -`.env.local` first and then `.env`; already-exported environment variables keep -highest priority. +materialized file is `0600` and gitignored. Live commands load +`.env.live-tokens`, `.env.local`, and `.env` in that order. Exported environment +variables keep highest priority. ```bash pnpm secrets:setup # one-time per worktree diff --git a/scripts/bootstrap-live-fixtures.ts b/scripts/bootstrap-live-fixtures.ts index 111794c..2c625d0 100644 --- a/scripts/bootstrap-live-fixtures.ts +++ b/scripts/bootstrap-live-fixtures.ts @@ -224,7 +224,7 @@ await runCheck("rss source fixture", async () => { const created = await primaryClient.rss.create({ dont_process_whole_feed: true, rss_source_url: rssSourceUrl, - title: `codex sdk rss fixture ${Date.now()}`, + title: `putio-typescript-sdk rss fixture ${Date.now()}`, }); try { diff --git a/scripts/bootstrap-tokens.ts b/scripts/bootstrap-tokens.ts index 7936b10..3e76d9a 100644 --- a/scripts/bootstrap-tokens.ts +++ b/scripts/bootstrap-tokens.ts @@ -1,12 +1,13 @@ import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; -import { hasLiveTokenCache, writeLiveTokenCache } from "./live-token-cache.ts"; +import { liveTokenCacheUrl, writeLiveTokenCache } from "./live-token-cache.ts"; import { bootstrapRuntimeTokens } from "../test/live/support/bootstrap.ts"; import { readBootstrapSecrets } from "../test/live/support/secrets.ts"; const refresh = process.argv.slice(2).includes("--refresh"); -if ((await hasLiveTokenCache()) && !refresh) { +if (existsSync(liveTokenCacheUrl) && !refresh) { throw new Error( "Live tokens are already cached; use test:live or pass --refresh to replace expired tokens", ); @@ -30,7 +31,7 @@ const bootstrapped = await bootstrapRuntimeTokens(secrets, async (config = {}) = createPutioSdkPromiseClient(config), ); -await writeLiveTokenCache({ +await writeLiveTokenCache(liveTokenCacheUrl, { firstPartyToken: bootstrapped.firstParty.accessToken, thirdPartyToken: bootstrapped.thirdParty.accessToken, }); diff --git a/scripts/live-error.spec.ts b/scripts/live-error.spec.ts deleted file mode 100644 index f057b74..0000000 --- a/scripts/live-error.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from "vite-plus/test"; - -import { formatLiveError } from "./live-error.ts"; - -describe("formatLiveError", () => { - it("formats normal errors", () => { - expect(formatLiveError(new Error("network unavailable"))).toBe("Error: network unavailable"); - }); - - it("formats tagged SDK errors without relying on their empty message", () => { - const error = Object.assign(new Error(), { - _tag: "PutioAuthError", - body: { - error_type: "TooManyRequests", - }, - retryAfter: "12", - status: 429, - }); - - expect(formatLiveError(error)).toBe("PutioAuthError status=429 TooManyRequests retryAfter=12"); - }); - - it("does not serialize unknown payload fields", () => { - expect( - formatLiveError({ - _tag: "PutioValidationError", - password: "do-not-print", - }), - ).toBe("PutioValidationError"); - }); -}); diff --git a/scripts/live-error.ts b/scripts/live-error.ts deleted file mode 100644 index e24f884..0000000 --- a/scripts/live-error.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Predicate } from "effect"; - -const nonEmptyString = (value: unknown): string | undefined => - Predicate.isString(value) && value.trim().length > 0 ? value.trim() : undefined; - -export const formatLiveError = (error: unknown): string => { - if (error instanceof Error) { - const message = nonEmptyString(error.message); - if (message !== undefined) { - return `${error.name}: ${message}`; - } - } - - if (Predicate.isObject(error)) { - const tag = nonEmptyString(error._tag) ?? nonEmptyString(error.name); - const status = Predicate.isNumber(error.status) ? String(error.status) : undefined; - const body = Predicate.isObject(error.body) ? error.body : undefined; - const errorType = body === undefined ? undefined : nonEmptyString(body.error_type); - const retryAfter = nonEmptyString(error.retryAfter); - const reset = nonEmptyString(error.reset); - const action = nonEmptyString(error.action); - const details = [ - tag, - status === undefined ? undefined : `status=${status}`, - errorType, - retryAfter === undefined ? undefined : `retryAfter=${retryAfter}`, - reset === undefined ? undefined : `reset=${reset}`, - action === undefined ? undefined : `action=${action}`, - ] - .filter(Predicate.isString) - .join(" "); - - if (details.length > 0) { - return details; - } - - if ("cause" in error) { - return formatLiveError(error.cause); - } - } - - return "Unknown live test failure"; -}; diff --git a/scripts/live-token-cache.spec.ts b/scripts/live-token-cache.spec.ts index 08dd7c2..1e93093 100644 --- a/scripts/live-token-cache.spec.ts +++ b/scripts/live-token-cache.spec.ts @@ -3,36 +3,27 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; -import { afterEach, describe, expect, it } from "vite-plus/test"; +import { describe, expect, it } from "vite-plus/test"; -import { hasLiveTokenCache, writeLiveTokenCache } from "./live-token-cache.ts"; - -const temporaryDirectories: string[] = []; - -afterEach(async () => { - await Promise.all( - temporaryDirectories.splice(0).map((path) => rm(path, { force: true, recursive: true })), - ); -}); +import { writeLiveTokenCache } from "./live-token-cache.ts"; describe("live token cache", () => { - it("writes ignored dotenv tokens with owner-only permissions", async () => { + it("writes dotenv tokens with 0600 permissions", async () => { const directory = await mkdtemp(join(tmpdir(), "putio-sdk-live-tokens-")); - temporaryDirectories.push(directory); - const url = pathToFileURL(join(directory, ".env.live-tokens")); - await writeLiveTokenCache( - { + try { + const url = pathToFileURL(join(directory, ".env.live-tokens")); + await writeLiveTokenCache(url, { firstPartyToken: "first-test-token", thirdPartyToken: "third-test-token", - }, - url, - ); + }); - expect(await hasLiveTokenCache(url)).toBe(true); - expect(await readFile(url, "utf8")).toBe( - 'PUTIO_TOKEN_FIRST_PARTY="first-test-token"\n' + - 'PUTIO_TOKEN_THIRD_PARTY="third-test-token"\n', - ); - expect((await stat(url)).mode & 0o777).toBe(0o600); + expect(await readFile(url, "utf8")).toBe( + 'PUTIO_TOKEN_FIRST_PARTY="first-test-token"\n' + + 'PUTIO_TOKEN_THIRD_PARTY="third-test-token"\n', + ); + expect((await stat(url)).mode & 0o777).toBe(0o600); + } finally { + await rm(directory, { force: true, recursive: true }); + } }); }); diff --git a/scripts/live-token-cache.ts b/scripts/live-token-cache.ts index 0b8653d..404133c 100644 --- a/scripts/live-token-cache.ts +++ b/scripts/live-token-cache.ts @@ -1,26 +1,13 @@ -import { access, chmod, writeFile } from "node:fs/promises"; +import { chmod, writeFile } from "node:fs/promises"; export const liveTokenCacheUrl = new URL("../.env.live-tokens", import.meta.url); -export const hasLiveTokenCache = async (url: URL = liveTokenCacheUrl): Promise => { - try { - await access(url); - return true; - } catch (error) { - if (error instanceof Error && "code" in error && error.code === "ENOENT") { - return false; - } - - throw error; - } -}; - export const writeLiveTokenCache = async ( + url: URL, tokens: { readonly firstPartyToken: string; readonly thirdPartyToken: string; }, - url: URL = liveTokenCacheUrl, ): Promise => { const contents = [ `PUTIO_TOKEN_FIRST_PARTY=${JSON.stringify(tokens.firstPartyToken)}`, diff --git a/scripts/test-live-targets.ts b/scripts/test-live-targets.ts index a29f634..6a90ed5 100644 --- a/scripts/test-live-targets.ts +++ b/scripts/test-live-targets.ts @@ -2,7 +2,6 @@ import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { relative, resolve, sep } from "node:path"; -import { formatLiveError } from "./live-error.ts"; import { readLiveTokens } from "../test/live/support/secrets.ts"; const args = process.argv.slice(2); @@ -38,7 +37,7 @@ const result = spawnSync("vp", ["test", "run", "--config", "vitest.live.config.t }); if (result.error) { - console.error(`Live test execution failed: ${formatLiveError(result.error)}`); + console.error(`Live test execution failed: ${result.error.message}`); } process.exit(result.status ?? 1); diff --git a/src/domains/auth.spec.ts b/src/domains/auth.spec.ts index f4a22c3..0ea4e02 100644 --- a/src/domains/auth.spec.ts +++ b/src/domains/auth.spec.ts @@ -48,12 +48,12 @@ describe("auth domain", () => { expect( buildAuthLoginUrl({ clientId: 42, - clientName: "Codex", + clientName: "putio-typescript-sdk", redirectUri: "https://example.com/callback", state: "state-123", }), ).toBe( - "https://app.put.io/authenticate?client_id=42&client_name=Codex&isolated=1&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&response_type=token&state=state-123", + "https://app.put.io/authenticate?client_id=42&client_name=putio-typescript-sdk&isolated=1&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&response_type=token&state=state-123", ); }); @@ -144,7 +144,7 @@ describe("auth domain", () => { login({ callbackUrl: "https://example.com/callback", clientId: 42, - clientName: "Codex", + clientName: "putio-typescript-sdk", clientSecret: "secret", fingerprint: "fingerprint-1", password: "pass", @@ -152,7 +152,7 @@ describe("auth domain", () => { }), (request) => { expect(request.url).toBe( - "https://api.put.io/v2/oauth2/authorizations/clients/42/fingerprint-1?callback_url=https%3A%2F%2Fexample.com%2Fcallback&client_name=Codex&client_secret=secret", + "https://api.put.io/v2/oauth2/authorizations/clients/42/fingerprint-1?callback_url=https%3A%2F%2Fexample.com%2Fcallback&client_name=putio-typescript-sdk&client_secret=secret", ); expect(getAuthorizationHeader(request)).toBe("Basic c2RrOnBhc3M="); diff --git a/test/live/account.test.ts b/test/live/account.test.ts index 929e6cc..93ba06c 100644 --- a/test/live/account.test.ts +++ b/test/live/account.test.ts @@ -70,7 +70,7 @@ describe.sequential("account live", () => { test("invalid callback url yields a typed operation error", async () => { await expect( clients.authClient.account.saveSettings({ - callback_url: "codex-invalid-callback", + callback_url: "putio-typescript-sdk-invalid-callback", }), ).rejects.toMatchObject({ _tag: "PutioOperationError", diff --git a/test/live/domains/config.ts b/test/live/domains/config.ts index 22ce969..2aa2f7c 100644 --- a/test/live/domains/config.ts +++ b/test/live/domains/config.ts @@ -5,7 +5,7 @@ const { oauthClient } = await createClients({ }); const now = new Date().toISOString(); -const probeKey = "codex_sdk_config_probe"; +const probeKey = "putio-typescript-sdk-config-probe"; const probeValue = { enabled: true, tags: ["config", "effect", "sdk"], diff --git a/test/live/domains/download-links.ts b/test/live/domains/download-links.ts index a1074ee..1a812fe 100644 --- a/test/live/domains/download-links.ts +++ b/test/live/domains/download-links.ts @@ -87,9 +87,13 @@ const findProbeFile = async (): Promise => { } const upload = await client.files.upload({ - file: new File(["sdk download-links probe\n"], `codex_sdk_download_links_${Date.now()}.txt`, { - type: "text/plain", - }), + file: new File( + ["sdk download-links probe\n"], + `putio-typescript-sdk-download-links-${Date.now()}.txt`, + { + type: "text/plain", + }, + ), parentId: 0, }); @@ -104,7 +108,7 @@ const createCursorProbeFiles = async () => { const ids: number[] = []; for (let index = 0; index < 2; index += 1) { - const name = `codex_sdk_download_links_cursor_${Date.now()}_${index}.txt`; + const name = `putio-typescript-sdk-download-links-cursor-${Date.now()}-${index}.txt`; const upload = await client.files.upload({ file: new File([`sdk download-links cursor probe ${index}\n`], name, { type: "text/plain", diff --git a/test/live/domains/events.ts b/test/live/domains/events.ts index 7464645..7cb0ab6 100644 --- a/test/live/domains/events.ts +++ b/test/live/domains/events.ts @@ -6,10 +6,7 @@ const { authClient, oauthClient } = await createClients({ }); const live = createLiveHarness("events live"); -const { assert, assertErrorTag, assertOperationError, finish, run, sleep } = live; - -void assertOperationError; -void sleep; +const { assert, assertErrorTag, assertOperationError, finish, run } = live; await run("events list shape", async () => { const result = await oauthClient.events.list({ diff --git a/test/live/domains/family.ts b/test/live/domains/family.ts index e19cc10..36d96ff 100644 --- a/test/live/domains/family.ts +++ b/test/live/domains/family.ts @@ -156,7 +156,7 @@ await run("family positive invite lookup with secondary fixture", async () => { await run("family remove missing member yields 404", async () => { try { - await authClient.family.removeMember("codex-no-such-family-member"); + await authClient.family.removeMember("putio-typescript-sdk-no-such-family-member"); throw new Error("expected remove missing family member to fail"); } catch (error) { return assertOperationError(error, { @@ -169,7 +169,7 @@ await run("family remove missing member yields 404", async () => { await run("family remove member requires restricted scope for oauth token", async () => { try { - await oauthClient.family.removeMember("codex-no-such-family-member"); + await oauthClient.family.removeMember("putio-typescript-sdk-no-such-family-member"); throw new Error("expected app-token removeMember to fail"); } catch (error) { return assertOperationError(error, { @@ -183,7 +183,7 @@ await run("family remove member requires restricted scope for oauth token", asyn await run("family join bogus code yields known 403", async () => { try { - await authClient.family.join("codex-invalid-family-code"); + await authClient.family.join("putio-typescript-sdk-invalid-family-code"); throw new Error("expected join with bogus code to fail"); } catch (error) { const operationError = expectOperationError(error); @@ -216,7 +216,7 @@ await run("family join bogus code yields known 403", async () => { await run("family join requires restricted scope for oauth token", async () => { try { - await oauthClient.family.join("codex-invalid-family-code"); + await oauthClient.family.join("putio-typescript-sdk-invalid-family-code"); throw new Error("expected join with app token to fail"); } catch (error) { return assertOperationError(error, { diff --git a/test/live/domains/file-direct.ts b/test/live/domains/file-direct.ts index 49e2f12..064624a 100644 --- a/test/live/domains/file-direct.ts +++ b/test/live/domains/file-direct.ts @@ -12,7 +12,7 @@ void assertOperationError; void sleep; const createDisposableTextFile = async (label: string) => { - const name = `codex_sdk_${label}_${Date.now()}.txt`; + const name = `putio-typescript-sdk-${label}-${Date.now()}.txt`; const upload = await client.files.upload({ file: new File(["sdk live probe\n"], name, { type: "text/plain", @@ -102,7 +102,7 @@ await run("files api mp4 download url redirects for owned video", async () => { }); const url = await client.files.getApiMp4DownloadUrl(video.id, { - name: "codex-sdk-live.mp4", + name: "putio-typescript-sdk-live.mp4", }); const response = await fetch(url, { redirect: "manual", @@ -154,7 +154,7 @@ await run("files XSPF playlist is fetchable for owned video", async () => { }); await run("files upload works through upload.put.io", async () => { - const name = `codex_sdk_upload_probe_${Date.now()}.txt`; + const name = `putio-typescript-sdk-upload-probe-${Date.now()}.txt`; const upload = await client.files.upload({ file: new File(["sdk upload probe\n"], name, { type: "text/plain", diff --git a/test/live/domains/file-tasks.ts b/test/live/domains/file-tasks.ts index 7c8a08c..3090330 100644 --- a/test/live/domains/file-tasks.ts +++ b/test/live/domains/file-tasks.ts @@ -15,9 +15,6 @@ const { authClient, client } = await createClients({ const live = createLiveHarness("file-tasks live"); const { assert, assertOperationError, finish, run, sleep } = live; -void assertOperationError; -void sleep; - const waitForExtractionTerminalState = async (id: number) => { for (let attempt = 0; attempt < 30; attempt += 1) { const extraction = (await client.files.listExtractions()).find((item) => item.id === id); @@ -66,11 +63,6 @@ await run("files setWatchStatus roundtrip", async () => { watched: false, }); } - - return { - checked: true, - video_id: video.id, - }; }); await run("files start_from roundtrip semantics", async () => { @@ -94,13 +86,6 @@ await run("files start_from roundtrip semantics", async () => { const restored = await client.files.getStartFrom(video.id); assert(restored === before, "expected start_from to be restored"); - - return { - before, - restored, - updated, - video_id: video.id, - }; } finally { await client.files.setStartFrom({ file_id: video.id, @@ -113,15 +98,15 @@ await run("files next-file natural ordering with disposable fixtures", async () const created: Array<{ readonly id: number; readonly name: string }> = []; const suffix = Date.now(); const folder = await authClient.files.createFolder({ - name: `codex_sdk_next_file_${suffix}`, + name: `putio-typescript-sdk-next-file-${suffix}`, parent_id: 0, }); try { for (const name of [ - `codex_sdk_episode_1_${suffix}.txt`, - `codex_sdk_episode_10_${suffix}.txt`, - `codex_sdk_episode_2_${suffix}.txt`, + `putio-typescript-sdk-episode-1-${suffix}.txt`, + `putio-typescript-sdk-episode-10-${suffix}.txt`, + `putio-typescript-sdk-episode-2-${suffix}.txt`, ]) { const upload = await authClient.files.upload({ file: new File(["sdk next-file probe\n"], name, { @@ -138,11 +123,11 @@ await run("files next-file natural ordering with disposable fixtures", async () } const episode1 = assertPresent( - created.find((file) => file.name.includes("_episode_1_")), + created.find((file) => file.name.includes("-episode-1-")), "expected episode 1 fixture", ); const episode2 = assertPresent( - created.find((file) => file.name.includes("_episode_2_")), + created.find((file) => file.name.includes("-episode-2-")), "expected episode 2 fixture", ); @@ -154,12 +139,6 @@ await run("files next-file natural ordering with disposable fixtures", async () } assert(next.id === episode2.id, "expected natural ordering to skip episode 10"); - - return { - from: episode1.name, - next: next.name, - next_id: next.id, - }; } finally { await authClient.files.delete([folder.id], { skipTrash: true, @@ -167,8 +146,8 @@ await run("files next-file natural ordering with disposable fixtures", async () } }); -await run("files owned archive extraction reaches terminal success", async () => { - const name = `codex_sdk_extract_${Date.now()}.zip`; +await run("files extract an owned archive", async () => { + const name = `putio-typescript-sdk-extract-${Date.now()}.zip`; const upload = await authClient.files.upload({ file: createStoredZipFile(name), fileName: name, @@ -179,26 +158,20 @@ await run("files owned archive extraction reaches terminal success", async () => throw new Error("expected archive upload to return a file"); } - const extractionIds: number[] = []; + let extractionId: number | undefined; const extractedFileIds: number[] = []; try { const created = await client.files.extract({ ids: [upload.file.id] }); assert(created.length === 1, "expected one extraction task"); const extraction = assertPresent(created[0], "expected extraction task"); - extractionIds.push(extraction.id); + extractionId = extraction.id; const terminal = await waitForExtractionTerminalState(extraction.id); assert(terminal.status === "EXTRACTED", "expected extraction to succeed"); extractedFileIds.push(...terminal.files); - - return { - archive_id: upload.file.id, - extracted_file_count: terminal.files.length, - final_status: terminal.status, - }; } finally { - for (const extractionId of extractionIds) { + if (extractionId !== undefined) { await client.files.deleteExtraction(extractionId).catch(() => undefined); } await authClient.files @@ -209,7 +182,7 @@ await run("files owned archive extraction reaches terminal success", async () => await run("files mp4 status on folder yields typed not-file", async () => { const folder = await authClient.files.createFolder({ - name: `codex_sdk_mp4_status_folder_${Date.now()}`, + name: `putio-typescript-sdk-mp4-status-folder-${Date.now()}`, parent_id: 0, }); diff --git a/test/live/domains/files.ts b/test/live/domains/files.ts index 295f70f..fdd4117 100644 --- a/test/live/domains/files.ts +++ b/test/live/domains/files.ts @@ -96,7 +96,9 @@ await run("files root list shape", async () => { }); await run("files list continue", async () => { - const probe = await createCursorProbeFolder(`codex_sdk_files_list_cursor_${Date.now()}`); + const probe = await createCursorProbeFolder( + `putio-typescript-sdk-files-list-cursor-${Date.now()}`, + ); try { const firstPage = await oauthClient.files.list(probe.folderId, { @@ -139,7 +141,7 @@ await run("files shared-with-you list", async () => { await run("files search and continue", async () => { const seed = Date.now(); - const query = `codex_sdk_files_search_cursor_${seed}`; + const query = `putio-typescript-sdk-files-search-cursor-${seed}`; const probe = await createCursorProbeFolder(query, 6); try { @@ -394,8 +396,8 @@ await run("files next-file and next-video", async () => { await run("files folder lifecycle", async () => { const suffix = Date.now(); - const folderAName = `codex_sdk_files_a_${suffix}`; - const folderBName = `codex_sdk_files_b_${suffix}`; + const folderAName = `putio-typescript-sdk-files-a-${suffix}`; + const folderBName = `putio-typescript-sdk-files-b-${suffix}`; const folderARenamed = `${folderAName}_renamed`; const folderA = await authClient.files.createFolder({ @@ -493,7 +495,7 @@ await run("empty folder name yields typed error", async () => { await run("folder mp4 status yields typed error", async () => { const folder = await authClient.files.createFolder({ - name: `codex_sdk_mp4_status_folder_${Date.now()}`, + name: `putio-typescript-sdk-mp4-status-folder-${Date.now()}`, parent_id: 0, }); diff --git a/test/live/domains/ifttt.ts b/test/live/domains/ifttt.ts index 11ae8f4..45adee4 100644 --- a/test/live/domains/ifttt.ts +++ b/test/live/domains/ifttt.ts @@ -30,7 +30,7 @@ await run("ifttt sendEvent negative behavior", async () => { try { await authClient.ifttt.sendEvent({ - eventType: "codex_invalid_event", + eventType: "putio-typescript-sdk-invalid-event", ingredients: { file_id: 1, }, @@ -64,7 +64,7 @@ await run("ifttt sendEvent rejects missing playback ingredients before transport eventType: "playback_started", ingredients: { file_id: 1, - file_name: "codex.mp4", + file_name: "putio-typescript-sdk.mp4", }, }); throw new Error("expected missing ingredients to fail"); @@ -79,7 +79,7 @@ await run("ifttt sendEvent requires restricted scope for oauth token", async () eventType: "playback_started", ingredients: { file_id: 1, - file_name: "codex.mp4", + file_name: "putio-typescript-sdk.mp4", file_type: "VIDEO", }, }); @@ -101,7 +101,7 @@ await run("ifttt valid playback event currently succeeds even when disabled", as eventType: "playback_started", ingredients: { file_id: 1, - file_name: "codex.mp4", + file_name: "putio-typescript-sdk.mp4", file_type: "VIDEO", }, }); diff --git a/test/live/domains/payment.ts b/test/live/domains/payment.ts index ab82a36..7ffa0e1 100644 --- a/test/live/domains/payment.ts +++ b/test/live/domains/payment.ts @@ -175,7 +175,7 @@ await run("payment invites require restricted scope", async () => { await run("payment fastspring confirm requires restricted scope", async () => { try { - await oauthClient.payment.confirmFastspringOrder("codex-bogus-reference"); + await oauthClient.payment.confirmFastspringOrder("putio-typescript-sdk-bogus-reference"); throw new Error("expected confirmFastspringOrder to fail with invalid_scope"); } catch (error) { return assertOperationError(error, { @@ -189,7 +189,7 @@ await run("payment fastspring confirm requires restricted scope", async () => { await run("payment fastspring confirm bogus reference currently yields generic 500", async () => { try { - await ownerClient.payment.confirmFastspringOrder("codex-bogus-reference"); + await ownerClient.payment.confirmFastspringOrder("putio-typescript-sdk-bogus-reference"); throw new Error("expected bogus fastspring reference to fail"); } catch (error) { return assertErrorTag(error, { @@ -234,7 +234,7 @@ await run("payment preview invalid coupon yields typed 404", async () => { try { await ownerClient.payment.changePlan.preview({ - coupon_code: "codex_invalid_coupon", + coupon_code: "putio-typescript-sdk-invalid-coupon", payment_type: "credit-card", plan_path: "1TB_365_once", }); @@ -255,7 +255,7 @@ await run("payment preview invalid plan yields typed 404", async () => { try { await ownerClient.payment.changePlan.preview({ payment_type: "credit-card", - plan_path: "codex_invalid_plan_path", + plan_path: "putio-typescript-sdk-invalid-plan-path", }); throw new Error("expected previewChangePlan to fail"); } catch (error) { @@ -274,7 +274,7 @@ await run("payment submit invalid plan yields typed 404", async () => { try { await ownerClient.payment.changePlan.submit({ payment_type: "credit-card", - plan_path: "codex_invalid_plan_path", + plan_path: "putio-typescript-sdk-invalid-plan-path", }); throw new Error("expected submitChangePlan to fail"); } catch (error) { @@ -291,7 +291,7 @@ await run("payment voucher info invalid code yields typed 404", async () => { await getOwnerPaymentInfo(); try { - await ownerClient.payment.voucher.getInfo("codex-invalid-voucher"); + await ownerClient.payment.voucher.getInfo("putio-typescript-sdk-invalid-voucher"); throw new Error("expected getVoucherInfo to fail"); } catch (error) { return assertOperationError(error, { @@ -307,7 +307,7 @@ await run("payment redeem invalid code yields typed 404", async () => { await getOwnerPaymentInfo(); try { - await ownerClient.payment.voucher.redeem("codex-invalid-voucher"); + await ownerClient.payment.voucher.redeem("putio-typescript-sdk-invalid-voucher"); throw new Error("expected redeemVoucher to fail"); } catch (error) { return assertOperationError(error, { @@ -367,7 +367,7 @@ await run("payment opennode unknown plan yields typed 400", async () => { await getOwnerPaymentInfo(); try { - await ownerClient.payment.methods.createOpenNodeCharge("codex-invalid-plan"); + await ownerClient.payment.methods.createOpenNodeCharge("putio-typescript-sdk-invalid-plan"); throw new Error("expected createOpenNodeCharge to fail"); } catch (error) { return assertOperationError(error, { @@ -422,7 +422,7 @@ await run("payment sub-account voucher redeem is rejected", async () => { const subAccountClient = await getSubAccountClient(); try { - await subAccountClient.payment.voucher.redeem("codex-invalid-voucher"); + await subAccountClient.payment.voucher.redeem("putio-typescript-sdk-invalid-voucher"); throw new Error("expected redeemVoucher to reject sub-account fixture"); } catch (error) { return assertPaymentSubAccountRestriction(error, "redeemVoucher"); diff --git a/test/live/domains/rss.ts b/test/live/domains/rss.ts index b7e5cf4..4c98266 100644 --- a/test/live/domains/rss.ts +++ b/test/live/domains/rss.ts @@ -71,7 +71,7 @@ await run("rss create update lifecycle", async () => { dont_process_whole_feed: true, keyword: "", parent_dir_id: 0, - title: `codex sdk rss ${seed}`, + title: `putio-typescript-sdk rss ${seed}`, unwanted_keywords: "", }); @@ -84,7 +84,7 @@ await run("rss create update lifecycle", async () => { const fetched = await authClient.rss.get(created.id); assert(fetched.id === created.id, "expected fetched feed id"); - const updatedTitle = `codex sdk rss updated ${seed}`; + const updatedTitle = `putio-typescript-sdk rss updated ${seed}`; await authClient.rss.update(created.id, { delete_old_files: true, @@ -138,7 +138,7 @@ await run("rss create update lifecycle", async () => { await run("rss create is writable with oauth token", async () => { const createdProbe = await createProbeRssFeed(oauthClient, { - title: `codex oauth rss ${Date.now()}`, + title: `putio-typescript-sdk oauth rss ${Date.now()}`, }); const created = createdProbe.feed; @@ -155,7 +155,7 @@ await run("rss invalid url yields typed error", async () => { try { await authClient.rss.create({ rss_source_url: "not-a-url", - title: "codex invalid rss", + title: "putio-typescript-sdk invalid rss", }); throw new Error("expected invalid URL to fail"); } catch (error) { @@ -291,7 +291,7 @@ await run("rss missing update feed yields typed not found", async () => { try { await authClient.rss.update(2147483647, { rss_source_url: rssSourceUrl, - title: "codex missing rss", + title: "putio-typescript-sdk missing rss", }); throw new Error("expected missing update feed to fail"); } catch (error) { diff --git a/test/live/domains/sharing.ts b/test/live/domains/sharing.ts index 886d968..d8477b4 100644 --- a/test/live/domains/sharing.ts +++ b/test/live/domains/sharing.ts @@ -32,7 +32,7 @@ const cleanupOwnedProbeFiles = async (ids: readonly number[]): Promise => }; const createOwnedProbeFile = async (): Promise => { - const name = `codex_sdk_public_share_probe_${Date.now()}.txt`; + const name = `putio-typescript-sdk-public-share-probe-${Date.now()}.txt`; const upload = await authClient.files.upload({ file: new File(["sdk public share probe\n"], name, { type: "text/plain", @@ -100,7 +100,7 @@ await run("sharing list shared files shape", async () => { await run("sharing everyone lifecycle", async () => { const seed = Date.now(); const folder = await authClient.files.createFolder({ - name: `codex_sdk_sharing_everyone_${seed}`, + name: `putio-typescript-sdk-sharing-everyone-${seed}`, parent_id: 0, }); @@ -138,7 +138,7 @@ await run("sharing specific-friend lifecycle", async () => { const seed = Date.now(); const folder = await authClient.files.createFolder({ - name: `codex_sdk_sharing_friend_${seed}`, + name: `putio-typescript-sdk-sharing-friend-${seed}`, parent_id: 0, }); @@ -191,11 +191,11 @@ await run("sharing child of shared parent yields typed already shared", async () const seed = Date.now(); const parent = await authClient.files.createFolder({ - name: `codex_sdk_sharing_parent_${seed}`, + name: `putio-typescript-sdk-sharing-parent-${seed}`, parent_id: 0, }); const child = await authClient.files.createFolder({ - name: `codex_sdk_sharing_child_${seed}`, + name: `putio-typescript-sdk-sharing-child-${seed}`, parent_id: parent.id, }); @@ -347,17 +347,17 @@ await run("sharing public share access without token yields configuration error" await run("sharing public share pagination mirrors backend contract", async () => { const seed = Date.now(); const folder = await authClient.files.createFolder({ - name: `codex_sdk_public_share_pagination_${seed}`, + name: `putio-typescript-sdk-public-share-pagination-${seed}`, parent_id: 0, }); try { const childA = await authClient.files.createFolder({ - name: `codex_sdk_public_share_child_a_${seed}`, + name: `putio-typescript-sdk-public-share-child-a-${seed}`, parent_id: folder.id, }); const childB = await authClient.files.createFolder({ - name: `codex_sdk_public_share_child_b_${seed}`, + name: `putio-typescript-sdk-public-share-child-b-${seed}`, parent_id: folder.id, }); @@ -498,7 +498,7 @@ await run("sharing clone lifecycle", async () => { ); const destination = await authClient.files.createFolder({ - name: `codex_sdk_clone_target_${Date.now()}`, + name: `putio-typescript-sdk-clone-target-${Date.now()}`, parent_id: 0, }); diff --git a/test/live/domains/transfers.ts b/test/live/domains/transfers.ts index 3bb20b7..82565ef 100644 --- a/test/live/domains/transfers.ts +++ b/test/live/domains/transfers.ts @@ -9,9 +9,6 @@ const { authClient, client } = await createClients({ const live = createLiveHarness("transfers live"); const { assert, assertErrorTag, assertOperationError, finish, run, sleep } = live; -void assertOperationError; -void sleep; - const waitForTransferError = async (id: number) => { for (let attempt = 0; attempt < 8; attempt += 1) { const transfer = await client.transfers.get(id); @@ -89,32 +86,25 @@ await run("transfers info external analysis", async () => { await run("transfers addMany envelope shape", async () => { const result = await client.transfers.addMany([ { - url: `https://example.invalid/codex-transfer-${Date.now()}.iso`, + url: `https://example.invalid/putio-typescript-sdk-transfer-${Date.now()}.iso`, }, { url: "not-a-url", }, ]); - assert(Array.isArray(result.transfers), "expected transfers array"); - assert(Array.isArray(result.errors), "expected per-item errors array"); - assert(result.transfers.length >= 1, "expected at least one created transfer"); - - const firstError = result.errors[0]; - - for (const transfer of result.transfers) { - await client.transfers.cancel([transfer.id]).catch(() => undefined); - await client.transfers.remove({ ids: [transfer.id] }).catch(() => undefined); - await client.transfers.clean([transfer.id]).catch(() => undefined); + try { + assert(Array.isArray(result.transfers), "expected transfers array"); + assert(Array.isArray(result.errors), "expected per-item errors array"); + assert(result.transfers.length >= 1, "expected at least one created transfer"); + assert(result.errors.length === 1, "expected one invalid URL error"); + } finally { + for (const transfer of result.transfers) { + await client.transfers.cancel([transfer.id]).catch(() => undefined); + await client.transfers.remove({ ids: [transfer.id] }).catch(() => undefined); + await client.transfers.clean([transfer.id]).catch(() => undefined); + } } - - return { - behaves_like_partial: result.errors.length > 0, - first_error_type: firstError?.error_type ?? null, - first_transfer_id: result.transfers[0]?.id ?? null, - transfer_count: result.transfers.length, - error_count: result.errors.length, - }; }); await run("empty transfer url yields typed error", async () => { @@ -156,7 +146,7 @@ await run("missing transfer stop recording yields typed error", async () => { await run("non-live transfer stop recording yields typed not-recording", async () => { const created = await client.transfers.add({ - url: `https://example.invalid/codex-transfer-${Date.now()}.iso`, + url: `https://example.invalid/putio-typescript-sdk-transfer-${Date.now()}.iso`, }); try { @@ -199,7 +189,7 @@ await run("reannounce currently rejects with typed bad request", async () => { await run("non-torrent reannounce currently falls back to typed bad request", async () => { const created = await client.transfers.add({ - url: `https://example.invalid/codex-transfer-${Date.now()}.iso`, + url: `https://example.invalid/putio-typescript-sdk-transfer-${Date.now()}.iso`, }); try { @@ -218,11 +208,9 @@ await run("non-torrent reannounce currently falls back to typed bad request", as } }); -await run("transfers disposable lifecycle", async () => { - const countBefore = await client.transfers.count(); - +await run("failed URL transfer can be retried and cancelled", async () => { const created = await client.transfers.add({ - url: `https://example.invalid/codex-transfer-${Date.now()}.iso`, + url: `https://example.invalid/putio-typescript-sdk-transfer-${Date.now()}.iso`, }); try { @@ -243,31 +231,20 @@ await run("transfers disposable lifecycle", async () => { const retried = await client.transfers.retry(created.id); assert(retried.status !== "ERROR", "expected retry to leave terminal error state"); - const retriedError = await waitForTransferError(created.id); + await waitForTransferError(created.id); const fetched = await client.transfers.get(created.id); assert(fetched.id === created.id, "expected get to return created transfer"); await client.transfers.cancel([created.id]); - - const countAfter = await client.transfers.count(); - - return { - count_after_cancel: countAfter, - count_before: countBefore, - created_id: created.id, - final_status: errored.status, - retried_status: retried.status, - retried_terminal_status: retriedError.status, - }; } finally { await client.transfers.cancel([created.id]).catch(() => undefined); await client.transfers.clean([created.id]).catch(() => undefined); } }); -await run("uploaded torrent exposes decoded transfer and metainfo bytes", async () => { - const name = `codex_sdk_torrent_transfer_${Date.now()}`; +await run("torrent upload returns a decoded transfer and metainfo", async () => { + const name = `putio-typescript-sdk-torrent-transfer-${Date.now()}`; const upload = await authClient.files.upload({ file: createTorrentFile(name), fileName: `${name}.torrent`, @@ -281,17 +258,10 @@ await run("uploaded torrent exposes decoded transfer and metainfo bytes", async const transferId = upload.transfer.id; try { - const transfer = await waitForTorrentTransfer(transferId); + await waitForTorrentTransfer(transferId); const torrent = await authClient.transfers.getTorrent(transferId); assert(torrent.byteLength > 20, "expected torrent metainfo bytes"); assert(torrent[0] === 0x64, "expected bencoded torrent metainfo"); - - return { - status: transfer.status, - torrent_bytes: torrent.byteLength, - transfer_id: transferId, - type: transfer.type, - }; } finally { await authClient.transfers.cancel([transferId]).catch(() => undefined); await authClient.transfers.remove({ ids: [transferId] }).catch(() => undefined); diff --git a/test/live/domains/trash.ts b/test/live/domains/trash.ts index 66b5416..7a31506 100644 --- a/test/live/domains/trash.ts +++ b/test/live/domains/trash.ts @@ -124,7 +124,7 @@ await run("trash list shape", async () => { await run("trash continue invalid cursor yields typed 400", async () => { try { - await authClient.trash.continue("codex-invalid-trash-cursor", { + await authClient.trash.continue("putio-typescript-sdk-invalid-trash-cursor", { per_page: 1, }); throw new Error("expected invalid trash cursor to fail"); @@ -164,7 +164,7 @@ await run("trash disposable lifecycle", () => try { for (const suffix of ["a", "b", "c"]) { const folder = await authClient.files.createFolder({ - name: `codex_sdk_trash_${seed}_${suffix}`, + name: `putio-typescript-sdk-trash-${seed}-${suffix}`, parent_id: 0, }); @@ -265,7 +265,7 @@ await run("trash bulk restore restores multiple top-level entries", () => try { for (const suffix of ["bulk_a", "bulk_b"]) { const folder = await authClient.files.createFolder({ - name: `codex_sdk_trash_${seed}_${suffix}`, + name: `putio-typescript-sdk-trash-${seed}-${suffix}`, parent_id: 0, }); @@ -319,13 +319,13 @@ await run("trash child restore and delete reject non-toplevel entries", () => try { const parent = await authClient.files.createFolder({ - name: `codex_sdk_trash_${seed}_parent`, + name: `putio-typescript-sdk-trash-${seed}-parent`, parent_id: 0, }); parentId = parent.id; const child = await authClient.files.createFolder({ - name: `codex_sdk_trash_${seed}_child`, + name: `putio-typescript-sdk-trash-${seed}-child`, parent_id: parent.id, }); childId = child.id; diff --git a/test/live/domains/zips.ts b/test/live/domains/zips.ts index 8dd5f1d..c9b4840 100644 --- a/test/live/domains/zips.ts +++ b/test/live/domains/zips.ts @@ -49,7 +49,7 @@ const createCursorProbeFiles = async () => { const ids: number[] = []; for (let index = 0; index < 6; index += 1) { - const name = `codex_sdk_zip_cursor_${Date.now()}_${index}.txt`; + const name = `putio-typescript-sdk-zip-cursor-${Date.now()}-${index}.txt`; const upload = await authClient.files.upload({ file: new File([`sdk zip cursor probe ${index}\n`], name, { type: "text/plain", diff --git a/test/live/oauth.test.ts b/test/live/oauth.test.ts index fc3a202..9cbaad2 100644 --- a/test/live/oauth.test.ts +++ b/test/live/oauth.test.ts @@ -2,8 +2,8 @@ import { describe, expect, test } from "vite-plus/test"; import { createLiveTokenClients } from "./support/helpers.js"; -const DISPOSABLE_CALLBACK = "https://example.com/codex-sdk-live/oauth/callback"; -const DISPOSABLE_WEBSITE = "https://example.com/codex-sdk-live/oauth"; +const DISPOSABLE_CALLBACK = "https://example.com/putio-typescript-sdk-live/oauth/callback"; +const DISPOSABLE_WEBSITE = "https://example.com/putio-typescript-sdk-live/oauth"; const DISPOSABLE_ICON_BYTES = Uint8Array.from( Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9WlAb9sAAAAASUVORK5CYII=", @@ -61,9 +61,9 @@ describe.sequential("oauth live", () => { try { const created = await authClient.oauth.create({ callback: DISPOSABLE_CALLBACK, - description: `codex oauth disposable ${seed}`, + description: `putio-typescript-sdk oauth disposable ${seed}`, hidden: true, - name: `Codex SDK Disposable ${seed}`, + name: `put.io TypeScript SDK Disposable ${seed}`, website: DISPOSABLE_WEBSITE, }); @@ -76,7 +76,7 @@ describe.sequential("oauth live", () => { const updated = await authClient.oauth.update({ callback: `${DISPOSABLE_CALLBACK}?updated=${seed}`, - description: `codex oauth disposable updated ${seed}`, + description: `putio-typescript-sdk oauth disposable updated ${seed}`, hidden: false, id: created.app.id, website: `${DISPOSABLE_WEBSITE}?updated=${seed}`, diff --git a/test/live/support/bootstrap.ts b/test/live/support/bootstrap.ts index 879bd55..02bf5cd 100644 --- a/test/live/support/bootstrap.ts +++ b/test/live/support/bootstrap.ts @@ -49,9 +49,9 @@ export type BootstrappedTokens = { }; }; -const THIRD_PARTY_BOOTSTRAP_APP_NAME = "Codex SDK Live App"; -const THIRD_PARTY_BOOTSTRAP_CALLBACK = "https://example.com/codex-sdk-live/callback"; -const THIRD_PARTY_BOOTSTRAP_WEBSITE = "https://example.com/codex-sdk-live"; +const THIRD_PARTY_BOOTSTRAP_APP_NAME = "put.io TypeScript SDK Live App"; +const THIRD_PARTY_BOOTSTRAP_CALLBACK = "https://example.com/putio-typescript-sdk-live/callback"; +const THIRD_PARTY_BOOTSTRAP_WEBSITE = "https://example.com/putio-typescript-sdk-live"; const TOTP_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/test/live/support/friends.ts b/test/live/support/friends.ts index 874c3c9..031facb 100644 --- a/test/live/support/friends.ts +++ b/test/live/support/friends.ts @@ -83,8 +83,8 @@ type SecondaryClientFactory = (config?: { readonly accessToken?: string; }) => Promise; -const SHARED_FRIEND_FIXTURE_FOLDER_NAME = "codex_sdk_shared_friend_fixture"; -const SHARED_FRIEND_FIXTURE_FILE_NAME = "codex_sdk_shared_friend_fixture.txt"; +const SHARED_FRIEND_FIXTURE_FOLDER_NAME = "putio-typescript-sdk-shared-friend-fixture"; +const SHARED_FRIEND_FIXTURE_FILE_NAME = "putio-typescript-sdk-shared-friend-fixture.txt"; const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/test/live/support/media.ts b/test/live/support/media.ts index 147f53c..3d87595 100644 --- a/test/live/support/media.ts +++ b/test/live/support/media.ts @@ -9,7 +9,7 @@ const SAFE_OWNED_VIDEO_FIXTURE_NAMES = new Set([ "Mario1_507_HQ_512kb.mp4", "Sintel.mp4", ]); -const SAFE_OWNED_VIDEO_FIXTURE_PREFIXES = ["codex_sdk_", "codex-sdk-"]; +const SAFE_OWNED_VIDEO_FIXTURE_PREFIXES = ["putio-typescript-sdk-"]; const isSafeOwnedVideoFixtureName = (name: string): boolean => SAFE_OWNED_VIDEO_FIXTURE_NAMES.has(name) || @@ -98,6 +98,6 @@ export const requireOwnedVideoFixture = async ( } throw new Error( - "Missing safe owned MP4 fixture. Set PUTIO_LIVE_OWNED_VIDEO_FILE_ID or upload an unshared codex_sdk_*/codex-sdk-* MP4 fixture.", + "Missing safe owned MP4 fixture. Set PUTIO_LIVE_OWNED_VIDEO_FILE_ID or upload an unshared putio-typescript-sdk-* MP4 fixture.", ); }; From f069f6f045fb7a4e8b2e139420a4aeadd97ad293 Mon Sep 17 00:00:00 2001 From: Altay Date: Sat, 29 Aug 2026 11:10:01 +0300 Subject: [PATCH 3/4] fix(tests): address live harness review findings --- .worktreeinclude | 1 + scripts/live-targets.spec.ts | 42 +++++++++++++++++++++++++++++++ scripts/live-targets.ts | 25 ++++++++++++++++++ scripts/test-live-targets.ts | 36 ++++++++++---------------- test/live/domains/transfers.ts | 16 +++++++----- test/live/support/secrets.test.ts | 13 +++++++--- test/live/support/secrets.ts | 12 +++++++-- 7 files changed, 110 insertions(+), 35 deletions(-) create mode 100644 scripts/live-targets.spec.ts create mode 100644 scripts/live-targets.ts diff --git a/.worktreeinclude b/.worktreeinclude index 079eec2..8152b36 100644 --- a/.worktreeinclude +++ b/.worktreeinclude @@ -1,2 +1,3 @@ /.env +/.env.live-tokens /.env.local diff --git a/scripts/live-targets.spec.ts b/scripts/live-targets.spec.ts new file mode 100644 index 0000000..cfee95d --- /dev/null +++ b/scripts/live-targets.spec.ts @@ -0,0 +1,42 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { describe, expect, it } from "vite-plus/test"; + +import { resolveLiveTestTargets } from "./live-targets.ts"; + +describe("resolveLiveTestTargets", () => { + it("accepts relative and absolute paths under test/live", async () => { + const cwd = await mkdtemp(join(tmpdir(), "putio-sdk-live-targets-")); + const target = join(cwd, "test/live/files.test.ts"); + + try { + await mkdir(join(cwd, "test/live"), { recursive: true }); + await writeFile(target, ""); + + expect(resolveLiveTestTargets(["./test/live/files.test.ts", target], cwd)).toEqual([ + target, + target, + ]); + } finally { + await rm(cwd, { force: true, recursive: true }); + } + }); + + it("rejects test files outside test/live", async () => { + const cwd = await mkdtemp(join(tmpdir(), "putio-sdk-live-targets-")); + const target = join(cwd, "test/files.test.ts"); + + try { + await mkdir(join(cwd, "test"), { recursive: true }); + await writeFile(target, ""); + + expect(() => resolveLiveTestTargets([target], cwd)).toThrow( + `Unsupported live test target: ${target}`, + ); + } finally { + await rm(cwd, { force: true, recursive: true }); + } + }); +}); diff --git a/scripts/live-targets.ts b/scripts/live-targets.ts new file mode 100644 index 0000000..2475f15 --- /dev/null +++ b/scripts/live-targets.ts @@ -0,0 +1,25 @@ +import { existsSync } from "node:fs"; +import { isAbsolute, relative, resolve, sep } from "node:path"; + +export const resolveLiveTestTargets = ( + targets: ReadonlyArray, + cwd = process.cwd(), +): ReadonlyArray => { + const liveRoot = resolve(cwd, "test/live"); + + return targets.map((target) => { + const resolvedTarget = resolve(cwd, target); + const relativeTarget = relative(liveRoot, resolvedTarget); + const isLiveTest = + relativeTarget.endsWith(".test.ts") && + relativeTarget !== ".." && + !relativeTarget.startsWith(`..${sep}`) && + !isAbsolute(relativeTarget); + + if (!isLiveTest || !existsSync(resolvedTarget)) { + throw new Error(`Unsupported live test target: ${target}`); + } + + return resolvedTarget; + }); +}; diff --git a/scripts/test-live-targets.ts b/scripts/test-live-targets.ts index 6a90ed5..4d09a3f 100644 --- a/scripts/test-live-targets.ts +++ b/scripts/test-live-targets.ts @@ -1,7 +1,6 @@ import { spawnSync } from "node:child_process"; -import { existsSync } from "node:fs"; -import { relative, resolve, sep } from "node:path"; +import { resolveLiveTestTargets } from "./live-targets.ts"; import { readLiveTokens } from "../test/live/support/secrets.ts"; const args = process.argv.slice(2); @@ -11,30 +10,21 @@ if (targets.length === 0) { throw new Error("Pass one or more explicit test/live/**/*.test.ts files"); } -const liveRoot = resolve("test/live"); - -for (const target of targets) { - const relativeTarget = relative(liveRoot, resolve(target)); - const isLiveTest = - target.startsWith("test/live/") && - target.endsWith(".test.ts") && - relativeTarget !== ".." && - !relativeTarget.startsWith(`..${sep}`); - - if (!isLiveTest || !existsSync(target)) { - throw new Error(`Unsupported live test target: ${target}`); - } -} +const resolvedTargets = resolveLiveTestTargets(targets); const tokens = readLiveTokens(); -const result = spawnSync("vp", ["test", "run", "--config", "vitest.live.config.ts", ...targets], { - env: { - ...process.env, - PUTIO_TOKEN_FIRST_PARTY: tokens.firstPartyToken, - PUTIO_TOKEN_THIRD_PARTY: tokens.thirdPartyToken, +const result = spawnSync( + "vp", + ["test", "run", "--config", "vitest.live.config.ts", ...resolvedTargets], + { + env: { + ...process.env, + PUTIO_TOKEN_FIRST_PARTY: tokens.firstPartyToken, + PUTIO_TOKEN_THIRD_PARTY: tokens.thirdPartyToken, + }, + stdio: "inherit", }, - stdio: "inherit", -}); +); if (result.error) { console.error(`Live test execution failed: ${result.error.message}`); diff --git a/test/live/domains/transfers.ts b/test/live/domains/transfers.ts index 82565ef..4f199bd 100644 --- a/test/live/domains/transfers.ts +++ b/test/live/domains/transfers.ts @@ -212,6 +212,7 @@ await run("failed URL transfer can be retried and cancelled", async () => { const created = await client.transfers.add({ url: `https://example.invalid/putio-typescript-sdk-transfer-${Date.now()}.iso`, }); + const transferIds = new Set([created.id]); try { assert(typeof created.id === "number", "expected created transfer id"); @@ -230,16 +231,19 @@ await run("failed URL transfer can be retried and cancelled", async () => { assert(typeof errored.error_message === "string", "expected transfer error message"); const retried = await client.transfers.retry(created.id); + transferIds.add(retried.id); assert(retried.status !== "ERROR", "expected retry to leave terminal error state"); - await waitForTransferError(created.id); + await waitForTransferError(retried.id); - const fetched = await client.transfers.get(created.id); - assert(fetched.id === created.id, "expected get to return created transfer"); + const fetched = await client.transfers.get(retried.id); + assert(fetched.id === retried.id, "expected get to return retried transfer"); - await client.transfers.cancel([created.id]); + await client.transfers.cancel([retried.id]); } finally { - await client.transfers.cancel([created.id]).catch(() => undefined); - await client.transfers.clean([created.id]).catch(() => undefined); + for (const id of transferIds) { + await client.transfers.cancel([id]).catch(() => undefined); + await client.transfers.clean([id]).catch(() => undefined); + } } }); diff --git a/test/live/support/secrets.test.ts b/test/live/support/secrets.test.ts index 1d94c1f..a2f51b6 100644 --- a/test/live/support/secrets.test.ts +++ b/test/live/support/secrets.test.ts @@ -7,29 +7,34 @@ import { describe, expect, it } from "vite-plus/test"; import { loadEnvFiles } from "./secrets.ts"; describe("live secret env loading", () => { - it("keeps process env first, then .env.local, then .env", () => { + it("keeps process env first, then token cache, .env.local, and .env", () => { const dir = mkdtempSync(join(tmpdir(), "putio-sdk-env-")); + const cachePath = join(dir, ".env.live-tokens"); const localPath = join(dir, ".env.local"); const envPath = join(dir, ".env"); const directKey = "PUTIO_SDK_TEST_DIRECT_PRECEDENCE"; + const cacheKey = "PUTIO_SDK_TEST_CACHE_PRECEDENCE"; const localKey = "PUTIO_SDK_TEST_LOCAL_PRECEDENCE"; const envKey = "PUTIO_SDK_TEST_ENV_FALLBACK"; const originalValues = new Map( - [directKey, localKey, envKey].map((key) => [key, process.env[key]]), + [directKey, cacheKey, localKey, envKey].map((key) => [key, process.env[key]]), ); try { process.env[directKey] = "direct"; + delete process.env[cacheKey]; delete process.env[localKey]; delete process.env[envKey]; + writeFileSync(cachePath, `${directKey}=cache\n${cacheKey}=cache\n`); writeFileSync(localPath, `${directKey}=local\n${localKey}=local\n`); - writeFileSync(envPath, `${directKey}=env\n${localKey}=env\n${envKey}=env\n`); + writeFileSync(envPath, `${directKey}=env\n${cacheKey}=env\n${localKey}=env\n${envKey}=env\n`); - loadEnvFiles([localPath, envPath]); + loadEnvFiles([cachePath, localPath, envPath]); expect(process.env[directKey]).toBe("direct"); + expect(process.env[cacheKey]).toBe("cache"); expect(process.env[localKey]).toBe("local"); expect(process.env[envKey]).toBe("env"); } finally { diff --git a/test/live/support/secrets.ts b/test/live/support/secrets.ts index 90395b8..ba35420 100644 --- a/test/live/support/secrets.ts +++ b/test/live/support/secrets.ts @@ -150,15 +150,23 @@ export const readFirstPartyClientCredentials = (): PutioClientCredentials => ({ }); export const hydrateLiveTokenEnv = (): void => { + loadPackageEnvFile(); + if (process.env.PUTIO_TOKEN_FIRST_PARTY && process.env.PUTIO_TOKEN_THIRD_PARTY) { return; } if (!process.env.PUTIO_TOKEN_FIRST_PARTY) { - process.env.PUTIO_TOKEN_FIRST_PARTY = readOptionalSecret("PUTIO_AUTH_TOKEN"); + const legacyToken = readOptionalSecret("PUTIO_AUTH_TOKEN"); + if (legacyToken) { + process.env.PUTIO_TOKEN_FIRST_PARTY = legacyToken; + } } if (!process.env.PUTIO_TOKEN_THIRD_PARTY) { - process.env.PUTIO_TOKEN_THIRD_PARTY = readOptionalSecret("PUTIO_OAUTH_TOKEN"); + const legacyToken = readOptionalSecret("PUTIO_OAUTH_TOKEN"); + if (legacyToken) { + process.env.PUTIO_TOKEN_THIRD_PARTY = legacyToken; + } } }; From a6374c1297df2b9a0a59b8d456a7f6217ac7bd48 Mon Sep 17 00:00:00 2001 From: Altay Date: Sat, 29 Aug 2026 12:18:33 +0300 Subject: [PATCH 4/4] fix(tests): close live runner safety gaps --- docs/TESTING.md | 4 +++- scripts/live-targets.spec.ts | 16 ++++++++++++++++ scripts/live-targets.ts | 13 +++++++++++++ test/live/domains/transfers.ts | 8 ++++++-- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index c8fb078..62bd223 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -109,6 +109,7 @@ Live tests stay separate on purpose: Default local env files, loaded in order: - direct process environment +- `.env.live-tokens` - `.env.local` - `.env` @@ -204,7 +205,8 @@ pnpm test:live:targets -- test/live/account.test.ts test/live/tunnel.test.ts `test:live:targets` runs only the named files. It reads `PUTIO_TOKEN_FIRST_PARTY` and `PUTIO_TOKEN_THIRD_PARTY` and never calls password -login. +login. It rejects `auth-credentials`, `family`, `friend-invites`, `friends`, +`podcast`, and `sharing` because those targets bootstrap account credentials. `pnpm bootstrap:tokens` writes new tokens to the ignored `0600` `.env.live-tokens` cache. Live commands load it before `.env.local`. Bootstrap diff --git a/scripts/live-targets.spec.ts b/scripts/live-targets.spec.ts index cfee95d..d3b0cec 100644 --- a/scripts/live-targets.spec.ts +++ b/scripts/live-targets.spec.ts @@ -39,4 +39,20 @@ describe("resolveLiveTestTargets", () => { await rm(cwd, { force: true, recursive: true }); } }); + + it("rejects targets that log in with account credentials", async () => { + const cwd = await mkdtemp(join(tmpdir(), "putio-sdk-live-targets-")); + const target = join(cwd, "test/live/podcast.test.ts"); + + try { + await mkdir(join(cwd, "test/live"), { recursive: true }); + await writeFile(target, ""); + + expect(() => resolveLiveTestTargets([target], cwd)).toThrow( + `Credential-backed target is not allowed: ${target}`, + ); + } finally { + await rm(cwd, { force: true, recursive: true }); + } + }); }); diff --git a/scripts/live-targets.ts b/scripts/live-targets.ts index 2475f15..53a61dd 100644 --- a/scripts/live-targets.ts +++ b/scripts/live-targets.ts @@ -1,6 +1,15 @@ import { existsSync } from "node:fs"; import { isAbsolute, relative, resolve, sep } from "node:path"; +const CREDENTIAL_BACKED_TARGETS = new Set([ + "auth-credentials.test.ts", + "family.test.ts", + "friend-invites.test.ts", + "friends.test.ts", + "podcast.test.ts", + "sharing.test.ts", +]); + export const resolveLiveTestTargets = ( targets: ReadonlyArray, cwd = process.cwd(), @@ -20,6 +29,10 @@ export const resolveLiveTestTargets = ( throw new Error(`Unsupported live test target: ${target}`); } + if (CREDENTIAL_BACKED_TARGETS.has(relativeTarget)) { + throw new Error(`Credential-backed target is not allowed: ${target}`); + } + return resolvedTarget; }); }; diff --git a/test/live/domains/transfers.ts b/test/live/domains/transfers.ts index 4f199bd..0e6921d 100644 --- a/test/live/domains/transfers.ts +++ b/test/live/domains/transfers.ts @@ -255,8 +255,12 @@ await run("torrent upload returns a decoded transfer and metainfo", async () => parentId: 0, }); - if (upload.type !== "transfer") { - throw new Error("expected torrent upload to return a transfer"); + if (upload.type === "file") { + try { + throw new Error("expected torrent upload to return a transfer"); + } finally { + await authClient.files.delete([upload.file.id], { skipTrash: true }); + } } const transferId = upload.transfer.id;