mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
bun loads a plain `.env` into process.env before anything runs, and tests/helpers/env.ts read that as a deliberate export, so it beat `.env.test` outright. `scripts/test.sh api` therefore pointed the api suites at whatever deployment `.env` describes: `prisma db push --accept-data-loss` for the schema, then a truncate of every table between tests. The e2e half was worse, because playwright.config.ts built and started the app with that DATABASE_URL and those R2 credentials, then wrote fixtures into it. CI never saw any of this: a runner has no `.env`. Three changes, in order of what each one catches: - helpers/dev-env.ts drops the values bun copied out of a development env file, leaving `.env.test` to fill them. Only values that match the file character-for-character go, so a real export still wins and the per-suite databases of a parallel api run keep working. - helpers/test-database.ts refuses a DATABASE_URL whose database name is not marked as a test one, at the single point every path into the setup passes through. This is the backstop, not the fix. - playwright.config.ts blanks the variables a development env file defines and the config does not. Dropping them from process.env is not enough there: `next build` and `next start` run @next/env themselves and read the files again. That is also why a local e2e run could not build at all (a set DISABLE_RATE_LIMIT throws in lib/rate-limit.ts under NODE_ENV=production) and why auth.spec.ts failed on a machine with SMTP configured. `scripts/test.sh` now creates `.env.test` from the committed example instead of asking for a one-line copy, so the guard above is something nobody has to meet.
233 lines
9.9 KiB
TypeScript
233 lines
9.9 KiB
TypeScript
import { defineConfig, devices } from '@playwright/test';
|
|
|
|
import {
|
|
developmentEnvKeys,
|
|
forgetAutoloadedDotenv,
|
|
readTestEnvValue,
|
|
} from './tests/helpers/dev-env';
|
|
import { assertTestDatabase } from './tests/helpers/test-database';
|
|
|
|
// Undo bun's automatic `.env` load before anything below reads process.env.
|
|
// APP_ENV is built while this file is evaluated, so without this the suite
|
|
// would build and start the app against whatever deployment `.env` describes,
|
|
// with that deployment's R2 credentials, and then write test fixtures into it.
|
|
//
|
|
// Note this is not `import './tests/helpers/env'`: that would pull all of
|
|
// `.env.test` in, DISABLE_RATE_LIMIT included, and the production `next build`
|
|
// below refuses to run with that set.
|
|
forgetAutoloadedDotenv();
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// End-to-end suite. See TESTING.md section 6.
|
|
//
|
|
// Report output: ./playwright-report (HTML), ./test-results (traces, videos).
|
|
// Both are gitignored and both are what the `e2e` job in ci.yml uploads.
|
|
//
|
|
// Port 3100, not 3000. The developer's dev server owns 3000 on this machine,
|
|
// and reuseExistingServer would happily attach the whole suite to it, pointing
|
|
// every test at the development database.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const PORT = Number(process.env.E2E_PORT ?? 3100);
|
|
|
|
/**
|
|
* Where the tests point their browser.
|
|
*
|
|
* Set E2E_BASE_URL to run against an app you started yourself (the `app-test`
|
|
* service in docker-compose.test.yml, for instance). Leaving it unset is the
|
|
* normal path: Playwright builds and starts the app itself, below.
|
|
*/
|
|
const BASE_URL = process.env.E2E_BASE_URL ?? `http://localhost:${PORT}`;
|
|
|
|
const MANAGES_OWN_SERVER = !process.env.E2E_BASE_URL;
|
|
|
|
/**
|
|
* Environment for the app under test.
|
|
*
|
|
* `.env.test` is deliberately not loaded here. Two reasons:
|
|
*
|
|
* 1. `next build` runs with NODE_ENV=production and never loads `.env.test`,
|
|
* and NEXT_PUBLIC_APP_URL is inlined into the client bundle at build time,
|
|
* so the build needs these values passed in explicitly anyway.
|
|
* 2. The R2_* variables below must NOT leak into the `api` Vitest project.
|
|
* `hasR2Config()` is derived from them, so putting them in `.env.test`
|
|
* would flip `isDirectFileUploadEnabled()` to true for 537 API tests that
|
|
* currently assert the unconfigured branch.
|
|
*
|
|
* DATABASE_URL is the exception, read out of `.env.test` on its own below: the
|
|
* app and the global setup that seeds it must never disagree about which
|
|
* database is under test.
|
|
*/
|
|
const DATABASE_URL =
|
|
process.env.DATABASE_URL ??
|
|
readTestEnvValue('DATABASE_URL') ??
|
|
'postgresql://openframe:openframe@postgres-test:5432/openframe_test?schema=public';
|
|
|
|
// The e2e suite registers users, uploads videos and deletes projects. Whatever
|
|
// it is pointed at ends up holding test fixtures, so it has to be a test
|
|
// database, and this is the last point before `next build` inherits the value.
|
|
assertTestDatabase(DATABASE_URL);
|
|
|
|
const APP_ENV: Record<string, string> = {
|
|
DATABASE_URL,
|
|
|
|
NEXTAUTH_URL: BASE_URL,
|
|
NEXT_PUBLIC_APP_URL: BASE_URL,
|
|
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET ?? 'test-secret-not-used-for-anything-real',
|
|
// Required. NextAuth v5 refuses every /api/auth/* request with
|
|
// `UntrustedHost` in production builds unless the host is trusted, which is
|
|
// why .env.docker.example sets the same variable for real deployments.
|
|
AUTH_TRUST_HOST: 'true',
|
|
|
|
// Stripe stays ON, with dummy credentials. With the flag off,
|
|
// hasBillingAccess() short-circuits to `true` and
|
|
// buildBillingAccessWhereInput() returns `{}`, so the billing gate that
|
|
// billing-gate.spec.ts exists to verify would not be armed at all. No spec
|
|
// walks into checkout, so no request ever reaches Stripe.
|
|
OPENFRAME_ENABLE_STRIPE: 'true',
|
|
STRIPE_SECRET_KEY: 'sk_test_openframe_dummy',
|
|
STRIPE_PRICE_ID: 'price_test_openframe_dummy',
|
|
STRIPE_WEBHOOK_SECRET: 'whsec_test_openframe_dummy',
|
|
|
|
OPENFRAME_REQUIRE_INVITE_CODE: 'true',
|
|
INVITE_CODE: 'test-invite',
|
|
TRUSTED_PROXY_MODE: 'none',
|
|
|
|
// Admin is not a database column. lib/auth.ts:143-148 derives `token.isAdmin`
|
|
// on every request by looking the signed-in address up in this list, so
|
|
// without it no account in this suite can be an admin and admin.spec.ts can
|
|
// only assert the refusals. The address is the one that spec signs in as.
|
|
ADMIN_EMAILS: process.env.ADMIN_EMAILS ?? '[email protected]',
|
|
|
|
// Direct video uploads through the MinIO service in docker-compose.test.yml.
|
|
// Without these the `Direct Upload` tab does not render at all, because
|
|
// app/(dashboard)/projects/[projectId]/videos/new/page.tsx passes
|
|
// isDirectFileUploadEnabled() into the client.
|
|
//
|
|
// The endpoint is the container hostname on purpose: the browser PUTs the
|
|
// file straight at the presigned URL, so the host the app signs for has to be
|
|
// the host the browser can resolve. Its origin is added to the CSP
|
|
// connect-src automatically by lib/content-security-policy.ts.
|
|
OPENFRAME_ENABLE_S3_VIDEO_UPLOADS: 'true',
|
|
OPENFRAME_ENABLE_BUNNY_UPLOADS: 'false',
|
|
R2_ENDPOINT: process.env.R2_ENDPOINT ?? 'http://minio-test:9000',
|
|
R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID ?? 'openframe',
|
|
R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY ?? 'openframe-test-secret',
|
|
R2_BUCKET_NAME: process.env.R2_BUCKET_NAME ?? 'openframe-test',
|
|
|
|
// Email verification must stay off, or a user registered through the form in
|
|
// auth.spec.ts cannot sign in until a message that nothing delivers has been
|
|
// clicked. isEmailVerificationEnabled() is derived from SMTP_HOST/USER/
|
|
// PASSWORD, so leaving those unset is what disables it. .env.test sets them
|
|
// for the api suite, which mocks nodemailer; nothing mocks it here.
|
|
};
|
|
|
|
// Blank every variable a development env file defines and this config does not.
|
|
//
|
|
// forgetAutoloadedDotenv() above cleared them out of process.env, which is not
|
|
// enough on its own: `next build` and `next start` run @next/env themselves and
|
|
// read `.env` again, filling anything still undefined. So on a developer machine
|
|
// the app under test would come up with that machine's configuration. SMTP_HOST
|
|
// alone turns email verification on and fails the registration spec, and
|
|
// DISABLE_RATE_LIMIT fails the production build outright, from inside
|
|
// lib/rate-limit.ts, reported as an unrelated "Failed to collect page data".
|
|
//
|
|
// An empty value rather than a deletion, because deletion is what @next/env
|
|
// undoes. The result is the environment CI already runs with, where these
|
|
// variables simply do not exist.
|
|
for (const key of developmentEnvKeys()) {
|
|
if (!(key in APP_ENV)) {
|
|
APP_ENV[key] = '';
|
|
}
|
|
}
|
|
|
|
export default defineConfig({
|
|
testDir: './tests/e2e',
|
|
outputDir: './test-results',
|
|
|
|
// Every spec seeds its own rows and deletes them again, so files are safe to
|
|
// interleave. What they share is one app process and one database.
|
|
fullyParallel: true,
|
|
// Capped rather than left to the core count: the limit is the single Next
|
|
// server, and the DB-backed rate limiter is keyed on the client IP, which is
|
|
// the same address for every worker.
|
|
workers: process.env.CI ? 2 : 4,
|
|
|
|
forbidOnly: !!process.env.CI,
|
|
retries: process.env.CI ? 2 : 0,
|
|
|
|
// A cold run has to build the app first, and `next build` on this codebase
|
|
// takes minutes; the per-test timeout is unrelated to that but the whole-run
|
|
// one is not.
|
|
timeout: 90_000,
|
|
expect: { timeout: 15_000 },
|
|
|
|
// `open: 'never'` matters locally too: the report server would otherwise hold
|
|
// the run open inside a container that has no browser to open it with.
|
|
reporter: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]],
|
|
|
|
globalSetup: './tests/e2e/global-setup.ts',
|
|
|
|
use: {
|
|
baseURL: BASE_URL,
|
|
trace: 'on-first-retry',
|
|
screenshot: 'only-on-failure',
|
|
video: 'off',
|
|
// Chromium in a container is slower than on a desktop, and the first
|
|
// navigation after a cold start pays for the route being compiled.
|
|
actionTimeout: 20_000,
|
|
navigationTimeout: 45_000,
|
|
},
|
|
|
|
projects: [
|
|
{
|
|
name: 'chromium',
|
|
use: { ...devices['Desktop Chrome'] },
|
|
testIgnore: '**/dashboard-mobile.spec.ts',
|
|
},
|
|
{
|
|
// One mobile project, for one spec. Section 6 asks for a mobile smoke
|
|
// test, not a second full pass.
|
|
name: 'mobile-chrome',
|
|
use: { ...devices['Pixel 7'] },
|
|
testMatch: '**/dashboard-mobile.spec.ts',
|
|
},
|
|
// Safari, for the one thing that genuinely differs there.
|
|
//
|
|
// Opt-in, because it is not free: the browser is a separate download and a
|
|
// second full pass would roughly double a CI run that is already the longest
|
|
// job. Enable it with E2E_WEBKIT=1; the weekly `mutation`-style schedule in
|
|
// ci.yml is the intended home for it rather than every push.
|
|
//
|
|
// Scoped to player.spec.ts on purpose. A video review tool's real Safari
|
|
// risk is playback: codec support, whether `currentTime` commits the way
|
|
// Chromium's does, and hls.js, none of which the other specs touch. Running
|
|
// all fourteen specs under WebKit would mostly re-test React.
|
|
...(process.env.E2E_WEBKIT
|
|
? [
|
|
{
|
|
name: 'webkit-player',
|
|
use: { ...devices['Desktop Safari'] },
|
|
testMatch: '**/player.spec.ts',
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
|
|
webServer: MANAGES_OWN_SERVER
|
|
? {
|
|
// `bun run build` would re-run `prebuild` (tsc --noEmit) on every cold
|
|
// start, which `bun run check` already covers. next is invoked through
|
|
// its bin so this works under both bun and node.
|
|
command: `./node_modules/.bin/next build && ./node_modules/.bin/next start -p ${PORT}`,
|
|
url: `${BASE_URL}/login`,
|
|
reuseExistingServer: !process.env.CI,
|
|
// A cold `next build` here measured a little over three minutes.
|
|
timeout: 15 * 60 * 1000,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
env: APP_ENV,
|
|
}
|
|
: undefined,
|
|
});
|