Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .worktreeinclude
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/.env
/.env.live-tokens
/.env.local
43 changes: 28 additions & 15 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -151,10 +152,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
Expand All @@ -168,6 +168,15 @@ 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.

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.

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
legacy local aliases when they are already exported in the shell.
Expand All @@ -188,34 +197,38 @@ 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 files. It reads
`PUTIO_TOKEN_FIRST_PARTY` and `PUTIO_TOKEN_THIRD_PARTY` and never calls password
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
refuses to replace the cache unless you pass `--refresh`.

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
`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.
Comment thread
altaywtf marked this conversation as resolved.

```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`
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@
"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",
"test:compat:browser:install": "playwright install chromium firefox webkit",
"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"
Expand Down
2 changes: 1 addition & 1 deletion scripts/bootstrap-live-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions scripts/bootstrap-tokens.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";

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 (existsSync(liveTokenCacheUrl) && !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"], {
Expand All @@ -21,6 +31,11 @@ const bootstrapped = await bootstrapRuntimeTokens(secrets, async (config = {}) =
createPutioSdkPromiseClient(config),
);

await writeLiveTokenCache(liveTokenCacheUrl, {
firstPartyToken: bootstrapped.firstParty.accessToken,
thirdPartyToken: bootstrapped.thirdParty.accessToken,
});

console.log(
JSON.stringify(
{
Expand Down
58 changes: 58 additions & 0 deletions scripts/live-targets.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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 });
}
});

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 });
}
});
});
38 changes: 38 additions & 0 deletions scripts/live-targets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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<string>,
cwd = process.cwd(),
): ReadonlyArray<string> => {
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}`);
}

if (CREDENTIAL_BACKED_TARGETS.has(relativeTarget)) {
throw new Error(`Credential-backed target is not allowed: ${target}`);
}

return resolvedTarget;
});
};
29 changes: 29 additions & 0 deletions scripts/live-token-cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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 { describe, expect, it } from "vite-plus/test";

import { writeLiveTokenCache } from "./live-token-cache.ts";

describe("live token cache", () => {
it("writes dotenv tokens with 0600 permissions", async () => {
const directory = await mkdtemp(join(tmpdir(), "putio-sdk-live-tokens-"));
try {
const url = pathToFileURL(join(directory, ".env.live-tokens"));
await writeLiveTokenCache(url, {
firstPartyToken: "first-test-token",
thirdPartyToken: "third-test-token",
});

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 });
}
});
});
20 changes: 20 additions & 0 deletions scripts/live-token-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { chmod, writeFile } from "node:fs/promises";

export const liveTokenCacheUrl = new URL("../.env.live-tokens", import.meta.url);
Comment thread
altaywtf marked this conversation as resolved.

export const writeLiveTokenCache = async (
url: URL,
tokens: {
readonly firstPartyToken: string;
readonly thirdPartyToken: string;
},
): Promise<void> => {
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);
};
99 changes: 0 additions & 99 deletions scripts/test-live-fresh.ts

This file was deleted.

Loading