Skip to content
Open
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
93 changes: 60 additions & 33 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
name: ci

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

env:
HUSKY: 0
Expand All @@ -15,28 +18,31 @@ jobs:
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"

- name: Install commitlint
run: npm install -D @commitlint/cli @commitlint/config-conventional
- name: Print versions
run: |
git --version
node --version
npm --version
npx commitlint --version

- name: Validate current commit (last commit) with commitlint
- run: npm ci
- run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
if: github.event_name == 'pull_request'
- run: npx commitlint --from ${{ github.event.before }} --to ${{ github.event.after }} --verbose
if: github.event_name == 'push'
run: npx commitlint --last --verbose

- name: Validate PR commits with commitlint
if: github.event_name == 'pull_request'
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
- run: npm ci
- run: |
npm run prettier:check
npm run lint
npm run fallow
Comment on lines +32 to +45

check-workflows:
permissions:
Expand All @@ -53,39 +59,51 @@ jobs:
- uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6

shellcheck:
needs:
- commitlint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Run shellcheck
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0
with:
severity: warning
- run: shellcheck scripts/*.sh

smoke:
needs:
- commitlint
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16
env:
POSTGRES_USER: authuser
POSTGRES_PASSWORD: authpass
POSTGRES_DB: authdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
- run: npm ci
- run: |
npm run prettier:check
npm run lint
npm run fallow
- run: npm run db:generate
- run: npm run db:migrate
- run: npm test

test:
env:
DATABASE_URL: postgres://authuser:authpass@localhost:5432/authdb
GITHUB_CLIENT_ID: dummy-id-for-testing
GITHUB_CLIENT_SECRET: dummy-secret-for-testing
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

e2e:
needs:
- commitlint
- test
runs-on: ubuntu-latest

services:
Expand All @@ -111,9 +129,18 @@ jobs:
with:
node-version-file: ".nvmrc"
- run: npm ci
- run: npx playwright install chromium-headless-shell
- run: npm run db:generate
- run: npm run db:migrate
- run: npm test -- --disable-coverage
- run: node scripts/migrate.js
- run: |
node src/index.js > server.log 2>&1 &
for i in {1..30}; do
curl -sf 'http://127.0.0.1:3001/health?type=startup' && break
sleep 1
done
cat server.log
npx playwright test

env:
DATABASE_URL: postgres://authuser:authpass@localhost:5432/authdb
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ lerna-debug.log*
# test coverage and tap
coverage/
.tap/
test-results/

# worktrees
.worktrees/
1 change: 1 addition & 0 deletions .taprc
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ jobs: 1
timeout: 30
coverage-report:
- "text"
allow-incomplete-coverage: true
68 changes: 68 additions & 0 deletions e2e/oauth-flow.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { test, expect } from "@playwright/test";
import { AUTH_DEFAULT_PORT, PLANNER_DEFAULT_PORT } from "../src/config.js";

const authBase = `http://localhost:${AUTH_DEFAULT_PORT}`;

test("OAuth 2.1 magic link flow", async ({ page, request }) => {
const email = `e2e-${Date.now()}@example.com`;

// Clear any stale magic links
await request.delete(`${authBase}/api/test/magic-links`);

// Start OAuth flow: unauthenticated user hits authorize endpoint with PKCE
const codeChallenge = "MJk6-W6P2z_PgOvWcEvbyqeIyc-GthZov8-QX37r0Vo";
const authorizeParams = new URLSearchParams({
client_id: "planner",
redirect_uri: `http://localhost:${PLANNER_DEFAULT_PORT}/auth/codebar/callback`,
response_type: "code",
state: "e2e-state-123",
scope: "openid profile",
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
await page.goto(
`${authBase}/api/auth/oauth2/authorize?${authorizeParams.toString()}`,
);

// OAuth provider redirects unauthenticated users to the login page
await expect(page).toHaveURL(/\/login/);

// Click through to the magic link form
await page.getByRole("button", { name: /Send Magic Link/i }).click();
await expect(page).toHaveURL(/\/login\/magic-link/);

// Submit email
await page.fill('input[name="email"]', email);
await page.click('button[type="submit"]');

// Success page is shown
await expect(page).toHaveURL(/\/login\/magic-link\?success=/);
await expect(page.locator("body")).toContainText("Magic link sent");

// Fetch the magic link from the auth dev endpoint
const linksRes = await request.get(`${authBase}/api/test/magic-links`);
const links = await linksRes.json();
const link = links.find((l) => l.email === email);
expect(link).toBeDefined();
expect(link.url).toMatch(/magic-link\/verify/);

// Call the magic link verify endpoint via API to establish a session.
// APIRequestContext automatically stores and sends cookies across requests.
const verifyRes = await request.get(link.url, { maxRedirects: 0 });
expect(verifyRes.status()).toBe(302);
const verifyLocation = verifyRes.headers()["location"];
expect(verifyLocation).toContain("/api/auth/oauth2/authorize");

// Call the authorize endpoint with the session cookie (don't follow redirects)
const authRes = await request.get(
`${authBase}/api/auth/oauth2/authorize?${authorizeParams.toString()}`,
{ maxRedirects: 0 },
);

// Should get a 302 redirect to the planner callback with the code
expect(authRes.status()).toBe(302);
const location = authRes.headers()["location"];
expect(location).toBeTruthy();
expect(location).toContain("code=");
expect(location).toContain("state=e2e-state-123");
});
Loading