Skip to content

Commit af7f02b

Browse files
committed
test(oauth): check OAuth scopes against a live server
Add `pnpm test:scopes`, which signs in and refreshes the token through the extension's OAuth flow against a throwaway deployment (test/scopes/compose.yaml). It checks that login and refresh both grant exactly the requested scopes, then runs a probe for every Coder API method the extension calls and for the requests `coder ssh` and `coder start` make, using the refreshed token. `scopeProbes.test.ts` runs in `pnpm test` and uses the TypeScript checker to find every CoderApi method src/ references, failing when one has no probe or listed reason. The workflow runs the suite on relevant PRs against a pinned coder-preview image, and nightly against `latest`. A failed nightly run comments on an open tracking issue, or opens one. Release images ignore requested scopes, and GHCR deletes preview images after a few weeks, so `pnpm sync:coder-preview` pins the newest one.
1 parent 01093dc commit af7f02b

15 files changed

Lines changed: 1049 additions & 15 deletions
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: OAuth Scopes
2+
3+
# PRs use the image pinned in compose.yaml; nightly runs use latest to catch
4+
# server changes early.
5+
on:
6+
pull_request:
7+
# Code the probes run or import
8+
paths:
9+
- src/**
10+
- test/mocks/**
11+
- test/scopes/**
12+
- package.json
13+
- pnpm-lock.yaml
14+
- vitest.config.mts
15+
- .github/workflows/oauth-scopes.yaml
16+
schedule:
17+
# Off the hour, when GitHub delays or drops fewer scheduled runs
18+
- cron: "17 6 * * *"
19+
workflow_dispatch:
20+
21+
permissions:
22+
contents: read
23+
24+
jobs:
25+
live-server:
26+
name: Live Server Test (coder-preview)
27+
runs-on: ubuntu-24.04
28+
timeout-minutes: 15
29+
permissions:
30+
contents: read
31+
issues: write
32+
steps:
33+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
34+
with:
35+
persist-credentials: false
36+
- uses: ./.github/actions/setup
37+
- run: docker compose -f test/scopes/compose.yaml up -d --wait
38+
env:
39+
CODER_IMAGE: ${{ github.event_name == 'schedule' && 'ghcr.io/coder/coder-preview:latest' || '' }}
40+
- run: pnpm test:scopes
41+
env:
42+
CODER_SCOPES_TEST_URL: http://localhost:7080
43+
# A failed scheduled run notifies only one person, so track it in an issue.
44+
- name: Report nightly failure
45+
if: failure() && github.event_name == 'schedule'
46+
env:
47+
GH_TOKEN: ${{ github.token }}
48+
GH_REPO: ${{ github.repository }}
49+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
50+
run: |
51+
title="Nightly OAuth scope test failed"
52+
issue=$(gh issue list --state open --search "\"$title\" in:title" --json number --jq '.[0].number')
53+
if [ -n "$issue" ]; then
54+
gh issue comment "$issue" --body "Failed again: $RUN_URL"
55+
else
56+
gh issue create --title "$title" --body "The scope test failed against \`coder-preview:latest\`: $RUN_URL"
57+
fi

‎AGENTS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ Before changing anything under `packages/` or `test/webview/`, read
4242
- Unit tests: `pnpm test`, or `pnpm test:extension` / `pnpm test:webview`
4343
with an optional file path
4444
- Integration tests: `pnpm test:integration`
45+
- OAuth scope tests: `pnpm test:scopes`, against a live server
46+
([docs/TESTING.md](docs/TESTING.md#oauth-scope-tests))
4547
- Storybook: `pnpm storybook`; theme snapshots: `pnpm sync:vscode-themes`
4648

4749
Integration tests and the theme sync launch VS Code; on headless machines

‎CONTRIBUTING.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ guides cover the rest; they serve humans and coding agents alike, and
88
naming, and test placement.
99
- [Architecture](docs/ARCHITECTURE.md): how opening a workspace works, SSH
1010
config management, Windows permissions and connection logging.
11-
- [Testing](docs/TESTING.md): unit, webview and integration tests, and
12-
testing the open flow by hand.
11+
- [Testing](docs/TESTING.md): unit, webview, integration and OAuth scope
12+
tests, and testing the open flow by hand.
1313
- [Tooling](docs/TOOLING.md): linting, formatting, TypeScript, Node.js and
1414
dependency upgrades.
1515
- [Webviews](packages/AGENTS.md): React, `@repo/ui`, Storybook and the

‎docs/TESTING.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ test/
5050
├── unit/ # Extension unit tests (mirrors src/)
5151
├── webview/ # Webview unit tests (mirrors packages/<pkg>/src/, jsdom)
5252
├── integration/ # Integration tests (real VS Code, Mocha)
53+
├── scopes/ # OAuth scopes against a live server
5354
├── utils/ # Test utilities that are also tested
5455
└── mocks/ # Shared test mocks
5556
```
@@ -72,6 +73,30 @@ command with `xvfb-run -a`.
7273
run in that copy with its own user data directory, so an open VS Code
7374
doesn't interfere.
7475

76+
## OAuth scope tests
77+
78+
`test/scopes/` signs in and refreshes the token through the extension's OAuth
79+
flow against a live server, then probes each server call the extension makes.
80+
When `pnpm test` reports an unprobed method, add a probe to
81+
`test/scopes/probes.ts` and run the suite. It needs Docker and a fresh
82+
deployment, since it creates users and workspaces, and `pnpm test` leaves it
83+
out:
84+
85+
```bash
86+
docker compose -f test/scopes/compose.yaml up -d --wait
87+
CODER_SCOPES_TEST_URL=http://localhost:7080 pnpm test:scopes
88+
docker compose -f test/scopes/compose.yaml down -v # before the next run
89+
```
90+
91+
Pull requests run against the coder-preview image pinned in `compose.yaml`;
92+
the nightly run uses `latest` and reports failures in a "Nightly OAuth scope
93+
test failed" issue. Set `CODER_IMAGE` to test another image. Release images
94+
ignore requested scopes, so only preview images can catch a missing one.
95+
96+
GHCR deletes preview images after a few weeks. If the pinned image is gone,
97+
or to test a newer server, run `pnpm sync:coder-preview` to pin the newest
98+
one, then run the suite.
99+
75100
## Testing the open flow by hand
76101

77102
There are a few ways you can test the "Open in VS Code" flow:

‎package.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@
4040
"storybook": "storybook dev -p 6006 --config-dir .storybook",
4141
"storybook:build": "storybook build --config-dir .storybook",
4242
"storybook:ci": "storybook build --test --config-dir .storybook",
43+
"sync:coder-preview": "node scripts/sync-coder-preview.mjs",
4344
"sync:vscode-themes": "node .storybook/themes/sync.mjs",
44-
"test": "cross-env CI=true ELECTRON_RUN_AS_NODE=1 electron node_modules/vitest/vitest.mjs",
45+
"test": "cross-env CI=true ELECTRON_RUN_AS_NODE=1 electron node_modules/vitest/vitest.mjs --project !scopes",
4546
"test:extension": "cross-env ELECTRON_RUN_AS_NODE=1 electron node_modules/vitest/vitest.mjs --project extension",
4647
"test:integration": "pnpm build:test && node scripts/esbuild.mjs && vscode-test --config test/integration/.vscode-test.mjs",
48+
"test:scopes": "cross-env ELECTRON_RUN_AS_NODE=1 electron node_modules/vitest/vitest.mjs --project scopes",
4749
"test:webview": "cross-env ELECTRON_RUN_AS_NODE=1 electron node_modules/vitest/vitest.mjs --project webview",
4850
"typecheck": "concurrently -g -n extension,tests,packages,storybook \"tsc --noEmit\" \"tsc --noEmit -p test\" \"pnpm typecheck:packages\" \"tsc --noEmit -p .storybook\"",
4951
"typecheck:packages": "pnpm -r --filter \"./packages/*\" --parallel typecheck",

‎pnpm-workspace.yaml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ catalog:
2222
react-dom: ^19.3.0
2323
storybook: ^10.6.1
2424
storybook-addon-pseudo-states: ^10.6.1
25-
# typescript stays on the 6.0 API for typescript-eslint; TypeScript 7 runs `tsc`.
25+
# typescript stays on the 6.0 API for typescript-eslint and the compiler API in
26+
# test/unit/oauth/scopeProbes.test.ts; TypeScript 7 runs `tsc`.
2627
# https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/
2728
typescript: npm:@typescript/typescript6@^6.0.2
2829
vite: ^8.3.2

‎scripts/sync-coder-preview.mjs‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Pins test/scopes/compose.yaml to the newest coder-preview image. GHCR deletes
2+
// preview images after a few weeks, so rerun this when the pinned one is gone.
3+
import { execFileSync } from "node:child_process";
4+
import { readFileSync, writeFileSync } from "node:fs";
5+
6+
const IMAGE = "ghcr.io/coder/coder-preview";
7+
const COMPOSE = new URL("../test/scopes/compose.yaml", import.meta.url);
8+
9+
const docker = (...args) =>
10+
execFileSync("docker", args, { encoding: "utf8" }).trim();
11+
12+
docker("pull", "--quiet", `${IMAGE}:latest`);
13+
const [repoDigest, version] = docker(
14+
"image",
15+
"inspect",
16+
`${IMAGE}:latest`,
17+
"--format",
18+
'{{index .RepoDigests 0}} {{index .Config.Labels "org.opencontainers.image.version"}}',
19+
).split(" ");
20+
// The tag only makes the pin readable; the digest is what Docker pulls.
21+
const tag = version.replace("+", "-");
22+
const pinned = `${IMAGE}:${tag}@${repoDigest.split("@")[1]}`;
23+
24+
const compose = readFileSync(COMPOSE, "utf8");
25+
const updated = compose.replace(
26+
/(\$\{CODER_IMAGE:-)[^}]+(\})/,
27+
`$1${pinned}$2`,
28+
);
29+
if (updated === compose && !compose.includes(pinned)) {
30+
throw new Error("No CODER_IMAGE default found in test/scopes/compose.yaml");
31+
}
32+
writeFileSync(COMPOSE, updated);
33+
console.log(`Pinned ${pinned}`);

‎scripts/test-electron.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ pnpm --dir "$RUNNER_DIR" add "electron@$ELECTRON_VERSION"
3030

3131
echo "Running tests with Electron $ELECTRON_VERSION..."
3232
ELECTRON_RUN_AS_NODE=1 \
33-
"$RUNNER_DIR/node_modules/.bin/electron" node_modules/vitest/vitest.mjs
33+
"$RUNNER_DIR/node_modules/.bin/electron" node_modules/vitest/vitest.mjs --project "!scopes"

‎src/websocket/oneWayWebSocket.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export class OneWayWebSocket<
161161
}
162162
}
163163

164-
function rawDataToString(data: RawData): string {
164+
export function rawDataToString(data: RawData): string {
165165
if (Buffer.isBuffer(data)) {
166166
return data.toString("utf8");
167167
} else if (data instanceof ArrayBuffer) {

‎test/scopes/compose.yaml‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# A throwaway deployment for `pnpm test:scopes`; see docs/TESTING.md. Update
2+
# the coder image with `pnpm sync:coder-preview`.
3+
services:
4+
postgres:
5+
image: postgres:17
6+
environment:
7+
POSTGRES_USER: coder
8+
POSTGRES_PASSWORD: coder
9+
POSTGRES_DB: coder
10+
healthcheck:
11+
test: pg_isready -U coder
12+
interval: 2s
13+
retries: 30
14+
15+
coder:
16+
image: ${CODER_IMAGE:-ghcr.io/coder/coder-preview:2.38.0-devel-2d96e88ebe@sha256:e6f38cf2da013f68bced6006a8e9197611d41dd1ef2d036867e458df3c0893af}
17+
ports: ["127.0.0.1:7080:7080"]
18+
environment:
19+
CODER_PG_CONNECTION_URL: postgres://coder:coder@postgres:5432/coder?sslmode=disable
20+
CODER_HTTP_ADDRESS: 0.0.0.0:7080
21+
CODER_ACCESS_URL: http://localhost:7080
22+
CODER_OAUTH2_PROVIDER_ENABLE: "true"
23+
depends_on:
24+
postgres:
25+
condition: service_healthy
26+
healthcheck:
27+
test: curl -fs http://localhost:7080/healthz
28+
interval: 2s
29+
retries: 60

0 commit comments

Comments
 (0)