Files
insights/playwright/utils/sharedViewExpectations.ts
zeekay 40d120dbf7 refactor(api): say /v1/ in the Python callers
Every first-party Python caller and test now names the mount the API actually
answers on. Mechanical: URL literals only, and only where the /api/ was ours --
a preceding word character means it was a module path like insights/api/ or a
third-party host like slack.com/api/, and those are untouched.

Left saying /api/, each for a reason rather than for safety:
- products/warehouse_sources/.../sources/** is a catalogue of vendor endpoints
- services/llm-gateway deploys separately and is under a code freeze, so moving
  its two calls would break it during the skew
- the mcp_store and Slack/Teams redirect_uris are registered with third parties
  and must byte-match what was registered
- the Wizard and Raycast client-metadata paths are OAuth client_ids
- /api/surveys and /api/feature_flag/local_evaluation are SDK aliases the
  ingress routes to the Rust fleets, held by released SDK versions

Test URLconfs moved with the requests that call them, and the Datastore
user-selection fixtures now carry the leading slash that request.path has.
2026-08-08 18:38:44 -07:00

67 lines
2.8 KiB
TypeScript

/**
* Shared helpers for the `/shared/{token}` unauthenticated playwright tests
* (dashboard, insight, notebook). Centralises the network-leak assertion so a
* silent 401/403 against /v1/* fails the test even if the page still renders.
*
* The class of regression we keep tripping on is "shared mode silently leaks an
* authenticated request" — the page may still render, but the leak is the
* actual bug. Each unauth spec must assert on the network, not just the DOM.
*/
import { type BrowserContext, expect, type Page } from '@playwright/test'
interface RecordedFailure {
method: string
url: string
status: number
}
/**
* Open a Page in the supplied context and record EVERY /v1/* response with
* status >= 400. The recording is unfiltered — what to allow vs flag is the
* caller's job (see `expectNoTeamScopedApiLeaks` below).
*
* The expected use is to pass a fresh BrowserContext (created with empty
* storage/cookies) to guarantee a logged-out browser, then assert against the
* recorded failures after the page has settled.
*/
export async function openUnauthenticatedSharedPage(context: BrowserContext): Promise<{
unauthPage: Page
failedApiResponses: RecordedFailure[]
}> {
const unauthPage = await context.newPage()
const failedApiResponses: RecordedFailure[] = []
unauthPage.on('response', (response) => {
const url = response.url()
const status = response.status()
if (!url.includes('/v1/') || status < 400) {
return
}
failedApiResponses.push({ method: response.request().method(), url, status })
})
return { unauthPage, failedApiResponses }
}
/**
* Assert that no `/v1/(environments|projects)/{team}/{resource}` request
* 4xx'd while the shared page was rendering. Other paths (`/v1/flags/`,
* `/v1/users/@me/`, `/v1/organizations/...`, `/v1/user_home_settings/`,
* etc) are intentionally NOT covered here — they have legitimate unauth
* failure modes, and the `client_request_failure` insights event tags them
* with `is_shared_view` so production telemetry still surfaces leaks.
*
* NOTE: this filter is deliberately narrow to avoid false positives. If a
* regression leaks an org-scoped or user-scoped call, this assertion won't
* catch it — broaden the filter (or add a sibling assertion) when those
* paths become a concern.
*/
export function expectNoTeamScopedApiLeaks(failedApiResponses: ReadonlyArray<RecordedFailure>): void {
const leaked = failedApiResponses.filter(({ url }) => /\/api\/(environments|projects)\/\d+\//.test(url))
expect(leaked, `Unexpected team-scoped API failures in shared mode:\n${formatFailures(leaked)}`).toEqual([])
}
function formatFailures(failures: ReadonlyArray<RecordedFailure>): string {
return failures.map(({ method, status, url }) => ` ${method} ${url} -> ${status}`).join('\n')
}