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
54 changes: 54 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Integration Tests

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

jobs:
integration-tests:
name: Integration Tests (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
node-version: [22, 24]

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Build plugin
run: npm run build

# Linux loopback already covers 127.0.0.0/8; no alias setup needed (the
# setup-loopback.sh script targets macOS lo0 and is a no-op here).

- name: Install fixtures
run: npm run install:fixtures

- name: Run integration tests
run: npm run test:integration
timeout-minutes: 5
env:
HARPER_INTEGRATION_TEST_LOG_DIR: /tmp/harper-test-logs
FORCE_COLOR: '1'

- name: Upload Harper logs on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: harper-logs-node-${{ matrix.node-version }}
path: /tmp/harper-test-logs
retention-days: 7
10 changes: 2 additions & 8 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

strategy:
matrix:
node-version: [20, 22, 24]
node-version: [22, 24]

steps:
- name: Checkout code
Expand All @@ -34,15 +34,9 @@ jobs:
- name: Check formatting
run: npm run format:check

# Coverage was failing to run on Node 20, so run it only on 22+
- name: Run tests with coverage (Node 22+)
if: matrix.node-version >= 22
- name: Run tests with coverage
run: npm run test:coverage

- name: Run tests (Node 20)
if: matrix.node-version == 20
run: npm run test:node-twenty

test-bun:
name: Test with Bun
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ dist/
.env
.env.*
!.env.example

# Integration test fixtures
integrationTests/fixtures/**/node_modules
integrationTests/fixtures/**/package-lock.json
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
dist/
node_modules/
.claude/
integrationTests/fixtures/**/node_modules/
14 changes: 12 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,19 @@ request.session = {

## Testing

Node.js built-in test runner (`node:test`) and Bun. Tests import from compiled `dist/`. Use `node:assert/strict`. Bun uses a preload script (`.bun/preload.js`) to mock the `harper` module.
**Unit tests** (`test/`): Node.js built-in test runner (`node:test`) and Bun. Tests import from compiled `dist/`. Use `node:assert/strict`. Both runners mock the `harper` module to keep unit tests off the real RocksDB:

Test scripts use `$(find test -name '*.test.js' -type f)`-style globbing for Node 20 compatibility; once Node 20 support ends, switch to `node --test test` or `node --test "test/**/*.test.js"` on Node 22+.
- Bun: `.bun/preload.js` (`mock.module`).
- Node: `test/helpers/harper-mock.mjs` registered via `--import` in the npm test scripts. Importing real `harper` opens the system RocksDB at module-load time, and RocksDB doesn't allow multi-process read-write access — so `node --test`'s per-file subprocesses contend for the LOCK and surface flaky errors. Drop this mock once harper exposes a read-only RocksDB mode (in flight); RocksDB does support multi-process read-only access.

**Integration tests** (`integrationTests/`): Boot a real Harper child process with the plugin installed, via `@harperfast/integration-testing` (`harper-integration-test-run`). Tests are `.test.ts` (Node 22+ type stripping). Run order:

```bash
npm run install:fixtures # npm pack + install the plugin into each fixture
npm run test:integration
```

The fixture install uses `npm pack` (not a `file:` symlink) because Harper's default VM sandbox rejects symlinked plugins with "Can not load module outside of allowed path". Adding a new fixture: drop a directory under `integrationTests/fixtures/` with a `config.yaml` and a `package.json` (no deps — install-fixtures injects the OAuth tarball).

## Dependencies

Expand Down
3 changes: 3 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[test]
preload = ["./.bun/preload.js"]
# integrationTests/ uses @harperfast/integration-testing's harper-integration-test-run
# (Node test runner only); they are not unit tests and should not run under `bun test`.
root = "./test"
62 changes: 62 additions & 0 deletions integrationTests/env-var-substitution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Verifies that ${VAR} placeholders in the OAuth plugin's provider config
* are substituted with values from process.env when the plugin loads under
* Harper v5. Boots a real Harper instance with the OAuth plugin installed
* (via npm pack into the fixture) and asserts the substituted client_id
* appears on the authorization redirect.
*/
import { suite, test, before, after } from 'node:test';
import { strictEqual } from 'node:assert/strict';
import { join, dirname } from 'node:path';
import { createRequire } from 'node:module';
import { setupHarperWithFixture, teardownHarper, type ContextWithHarper } from '@harperfast/integration-testing';

const require = createRequire(import.meta.url);

function getHarperBinPath(): string {
return join(dirname(require.resolve('harper')), 'bin', 'harper.js');
}

const fixturePath = join(import.meta.dirname, 'fixtures', 'oauth-app');

const EXPECTED_CLIENT_ID = 'integration-test-client-id';
const EXPECTED_CLIENT_SECRET = 'integration-test-client-secret';

suite('OAuth plugin env-var substitution under Harper v5', (ctx: ContextWithHarper) => {
before(async () => {
await setupHarperWithFixture(ctx, fixturePath, {
harperBinPath: getHarperBinPath(),
env: {
OAUTH_TEST_CLIENT_ID: EXPECTED_CLIENT_ID,
OAUTH_TEST_CLIENT_SECRET: EXPECTED_CLIENT_SECRET,
},
config: {
logging: { stdStreams: true },
},
});
});

after(async () => {
await teardownHarper(ctx);
});

test('login redirect carries the substituted client_id', async () => {
const response = await fetch(`${ctx.harper.httpURL}/oauth/test-provider/login`, {
redirect: 'manual',
});
strictEqual(response.status, 302, `expected 302, got ${response.status}`);
const location = response.headers.get('location');
strictEqual(typeof location, 'string', 'Location header missing');
const url = new URL(location!);
strictEqual(
url.origin + url.pathname,
'http://example.test/authorize',
'authorization URL was not constructed from configured authorizationUrl'
);
strictEqual(
url.searchParams.get('client_id'),
EXPECTED_CLIENT_ID,
'client_id was not substituted from ${OAUTH_TEST_CLIENT_ID}'
);
});
});
19 changes: 19 additions & 0 deletions integrationTests/fixtures/oauth-app/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Integration test fixture: exercises ${VAR} substitution in OAuth provider config.
#
# The plugin's config.ts/expandEnvVar replaces ${VAR} placeholders with values
# from process.env. The test harness sets OAUTH_TEST_CLIENT_ID etc. on the
# Harper child process before boot; we then assert that the values reach the
# generated authorization URL.
rest: true

'@harperfast/oauth':
package: '@harperfast/oauth'
providers:
test-provider:
provider: generic
clientId: ${OAUTH_TEST_CLIENT_ID}
clientSecret: ${OAUTH_TEST_CLIENT_SECRET}
authorizationUrl: 'http://example.test/authorize'
tokenUrl: 'http://example.test/token'
userInfoUrl: 'http://example.test/userinfo'
scope: 'openid profile email'
6 changes: 6 additions & 0 deletions integrationTests/fixtures/oauth-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "oauth-app-fixture",
"private": true,
"type": "module",
"description": "Integration-test fixture. The @harperfast/oauth dep is installed at test setup time via scripts/install-fixtures.js (npm pack + install) so the plugin lives as real files under node_modules — required because Harper's default VM sandbox rejects symlinked file: deps with 'Can not load module outside of allowed path'."
}
15 changes: 15 additions & 0 deletions integrationTests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true
},
"include": ["**/*.ts"],
"exclude": ["fixtures/**/node_modules"]
}
Loading
Loading