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
29 changes: 23 additions & 6 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions framework-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Framework Tests

Integration tests that verify varlock works correctly with real framework build pipelines. Each test creates an isolated temporary project, installs packed varlock packages from source, runs a real build, and asserts on the output.

## Running tests

From this directory (`framework-tests/`):

```sh
# Run all framework tests
bun run test

# Run tests for a specific framework
bun run test:expo
bun run test:nextjs

# Watch mode (re-runs on file changes)
bun run test:watch

# Re-pack varlock packages after making source changes
bun run repack
```

Or from the repo root:

```sh
bun run --filter varlock-framework-tests test
```

## How it works

### Test harness (`harness/`)

The shared harness provides `FrameworkTestEnv`, which manages the full lifecycle:

1. **Pack** — varlock packages are built and packed into `.tgz` tarballs (cached in `.packed/`; run `bun run repack` to refresh after source changes)
2. **Setup** — a temp project is created in `.test-projects/`, deps are installed via pnpm
3. **Scenario** — template files are copied, a build command runs, and output is asserted
4. **Teardown** — temp project is removed (set `KEEP_TEST_DIRS=1` to preserve for debugging)

### Adding a new framework

Create a directory under `frameworks/<name>/` with:

```
frameworks/<name>/
<name>.test.ts # Test file using FrameworkTestEnv
files/
_base/ # Files copied into every scenario (config, build scripts, etc.)
schemas/ # .env.schema and env override files
pages/ # Page/component templates swapped per scenario
```

If the framework needs a new varlock integration package, register it in `harness/pack.ts`.

### Test scenarios

Each scenario uses `describeScenario()` which:
- Copies template files into the project (merging fixture defaults with scenario overrides)
- Runs a build command (auto-prefixed with the package manager)
- Creates individual vitest tests for each `fileAssertion` and `outputAssertion`

### Environment variables

| Variable | Description |
|---|---|
| `KEEP_TEST_DIRS` | Set to `1` to preserve temp project dirs after tests |
| `REPACK` | Set to `1` to force re-packing varlock tarballs (otherwise cached) |

## Current frameworks

- **Next.js** — tests multiple versions (14, 15, 16) and bundlers (webpack, turbopack), verifying env injection, leak detection, log redaction, and sourcemap scrubbing
- **Expo** — tests the babel plugin transform pipeline, verifying static replacement of public vars, protection of sensitive vars, and correct handling of server (+api) routes
222 changes: 222 additions & 0 deletions framework-tests/frameworks/expo/expo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import {
describe, beforeAll, afterAll,
} from 'vitest';
import { FrameworkTestEnv } from '../../harness/index';

describe('Expo Integration', () => {
const expoEnv = new FrameworkTestEnv({
testDir: import.meta.dirname,
framework: 'expo',
packageManager: 'pnpm',
dependencies: {
'@babel/core': '^7.25.0',
'@babel/preset-typescript': '^7.25.0',
varlock: 'will-be-replaced',
'@varlock/expo-integration': 'will-be-replaced',
},
templateFiles: {
'.env.schema': 'schemas/.env.schema',
},
});

beforeAll(() => expoEnv.setup(), 180_000);
afterAll(() => expoEnv.teardown());

describe('client page', () => {
expoEnv.describeScenario('basic page', {
command: 'node build.mjs',
templateFiles: {
'app/page.tsx': 'pages/basic-page.tsx',
},
fileAssertions: [
{
description: 'public env vars are statically replaced',
fileGlob: 'dist/**/*.js',
shouldContain: [
'"Varlock Expo Test"',
'"https://api.example.com"',
],
},
{
description: 'sensitive var is NOT inlined (secret value absent)',
fileGlob: 'dist/**/*.js',
shouldNotContain: ['super-secret-key-12345'],
},
{
description: 'sensitive var reference is preserved',
fileGlob: 'dist/**/*.js',
shouldContain: ['ENV.SECRET_KEY'],
},
],
outputAssertions: [
{
description: 'log line appears but secret value is redacted',
shouldContain: ['secret-log-test:'],
shouldNotContain: ['super-secret-key-12345'],
},
],
});

expoEnv.describeScenario('leaky page — secret value never appears in output', {
command: 'node build.mjs',
templateFiles: {
'app/page.tsx': 'pages/leaky-page.tsx',
},
fileAssertions: [
{
description: 'sensitive var is NOT inlined even when used directly',
fileGlob: 'dist/**/*.js',
shouldNotContain: ['super-secret-key-12345'],
},
{
description: 'sensitive var reference is preserved as ENV.SECRET_KEY',
fileGlob: 'dist/**/*.js',
shouldContain: ['ENV.SECRET_KEY'],
},
],
outputAssertions: [
{
description: 'build warns about sensitive var in client file',
shouldContain: ['@sensitive'],
},
],
});
});

describe('mixed client + server build', () => {
expoEnv.describeScenario('warns only for client file, not server route', {
command: 'node build.mjs',
templateFiles: {
'app/page.tsx': 'pages/mixed-page.tsx',
'app/data+api.ts': 'pages/server-route+api.ts',
},
fileAssertions: [
{
description: 'client page has public var replaced',
filePath: 'dist/page.js',
shouldContain: ['"Varlock Expo Test"'],
},
{
description: 'client page does not contain secret value',
filePath: 'dist/page.js',
shouldNotContain: ['super-secret-key-12345'],
},
{
description: 'server route has public var replaced',
filePath: 'dist/data+api.js',
shouldContain: ['"https://api.example.com"'],
},
{
description: 'server route does not contain secret value',
filePath: 'dist/data+api.js',
shouldNotContain: ['super-secret-key-12345'],
},
],
outputAssertions: [
{
description: 'warning mentions the client file path',
shouldContain: ['page.tsx'],
},
{
description: 'warning does not mention the server +api file',
shouldNotContain: ['data+api.ts'],
},
],
});
});

describe('empty sensitive var', () => {
expoEnv.describeScenario('empty optional sensitive var is not inlined', {
command: 'node build.mjs',
templateFiles: {
'app/page.tsx': 'pages/empty-secret-page.tsx',
},
fileAssertions: [
{
description: 'public var is still replaced',
fileGlob: 'dist/**/*.js',
shouldContain: ['"Varlock Expo Test"'],
},
{
description: 'empty sensitive var reference is preserved',
fileGlob: 'dist/**/*.js',
shouldContain: ['ENV.EMPTY_SECRET'],
},
],
});
});

describe('invalid schema', () => {
expoEnv.describeScenario('build fails on invalid config', {
command: 'node build.mjs',
expectSuccess: false,
templateFiles: {
'.env.schema': 'schemas/.env.schema.invalid',
'app/page.tsx': 'pages/basic-page.tsx',
},
outputAssertions: [
{
description: 'error mentions failed config load',
shouldContain: ['Failed to load varlock config'],
},
],
});
});

describe('server +api route', () => {
expoEnv.describeScenario('server route handles sensitive vars without warning', {
command: 'node build.mjs',
templateFiles: {
'app/page.tsx': 'pages/public-only-page.tsx',
'app/data+api.ts': 'pages/server-route+api.ts',
},
fileAssertions: [
{
description: 'public var is statically replaced in server route',
filePath: 'dist/data+api.js',
shouldContain: ['"https://api.example.com"'],
},
{
description: 'sensitive var is NOT inlined in server route',
filePath: 'dist/data+api.js',
shouldNotContain: ['super-secret-key-12345'],
},
{
description: 'sensitive var reference is preserved in server route',
filePath: 'dist/data+api.js',
shouldContain: ['ENV.SECRET_KEY'],
},
],
outputAssertions: [
{
description: 'no sensitive var warning for server +api files',
shouldNotContain: ['@sensitive'],
},
],
});
});

describe('metro config', () => {
expoEnv.describeScenario('initializes resolver and ENV proxy with real varlock', {
command: 'node test-metro-config.mjs',
outputAssertions: [
{
description: 'all metro-config checks pass',
shouldContain: ['All metro-config checks passed'],
},
{
description: 'resolver resolves varlock/env',
shouldContain: ['resolver: varlock/env'],
},
{
description: 'resolver falls through for non-varlock modules',
shouldContain: ['non-varlock fallthrough OK'],
},
{
description: 'ENV proxy returns real values',
shouldContain: ['ENV.APP_NAME = Varlock Expo Test', 'ENV.API_URL = https://api.example.com'],
},
],
});
});
});
Loading
Loading