Last Updated: 2025-12-02 Status: ✅ All tests passing (51 assertions)
The auth server uses GitHub Actions for continuous integration and deployment. Every push to any branch triggers automated testing to ensure code quality and OAuth 2.1/OIDC compliance.
Located in .github/workflows/:
test-auth.yml- Main test pipeline- Runs on: Push to any branch
- Duration: ~2-3 minutes
- Jobs: Auth Server Tests, Security Audit
File: tests/test-oauth-flows.js
Tests core OAuth 2.1 and OIDC functionality:
- ✅ OIDC Discovery (
.well-known/openid-configuration) - ✅ JWKS Endpoint (
/api/auth/jwks) - ✅ PKCE Flow (Public Client with code challenge)
- ✅ Confidential Client Flow (with client secret)
- ✅ Userinfo Endpoint (user claims retrieval)
- ✅ ID Token Structure (JWT validation)
- ✅ Dynamic Client Registration (disabled for security)
Run locally:
node tests/test-oauth-flows.jsFile: tests/test-tenant-claims.js
Validates multi-tenancy JWT claims:
- ✅
tenant_id,organization_ids,org_rolepresent in userinfo
Run locally:
node tests/test-tenant-claims.jsFile: tests/test-edge-cases.js
Security and error handling validation:
Security Tests:
- ✅ Invalid client_id handling (302 redirect)
- ✅ Mismatched redirect_uri rejection (400 error)
- ✅ PKCE verifier mismatch rejection (401 error)
- ✅ Authorization code reuse rejection (401 error)
- ✅ Invalid access token rejection (401 error)
- ✅ Missing auth header rejection (401 error)
Authentication Tests:
- ✅ Wrong password rejection (401 error)
- ✅ Non-existent email rejection (401 error)
- ✅ Invalid email format rejection (400 error)
Flow Tests:
- ✅ Unauthenticated authorize handling (200 with redirect)
- ✅ Refresh token flow (new access token issued)
- ✅ Invalid grant_type rejection (400 error)
- ✅ Invalid scope handling (200 - graceful degradation)
Run locally:
node tests/test-edge-cases.jsFile: tests/test-tenant-edge-cases.js
Multi-tenancy specific validation:
- ✅ User with organization has tenant claims
- ✅
tenant_idmatches primary organization - ✅
org_rolepresent for organization members - ✅ Profile claims present (software background, hardware tier)
- ✅ Role claim valid
- ✅
email_verifiedis boolean - ✅ ID token has standard claims (iss, sub, aud, exp, iat)
- ✅
organization_idsis array type
Run locally:
node tests/test-tenant-edge-cases.jsFile: tests/test-confidential-client.js
Server-to-server OAuth flow validation:
- ✅ Authorization code exchange with client secret
- ✅ Client secret authentication (Basic Auth)
- ✅ Access token issuance
- ✅ User info retrieval
- ✅ Tenant claims present in response
- ✅ Refresh token working (with
offline_accessscope)
Run locally:
node tests/test-confidential-client.jsFile: tests/test-default-organization.js
Hybrid multi-tenant model validation:
- ✅ User signup auto-joins default organization
- ✅ JWT token includes correct
tenant_id - ✅ Duplicate membership prevention (idempotency)
- ✅ Default organization exists (startup validation)
Run locally:
node tests/test-default-organization.jsFile: tests/oauth-validation.test.ts
Standards compliance validation:
Passing Checks (9):
- ✅ Discovery document issuer matches
BETTER_AUTH_URL - ✅ Authorization endpoint uses correct base URL
- ✅ Token endpoint uses correct base URL
- ✅ Userinfo endpoint uses correct base URL
- ✅ JWKS URI uses correct base URL
- ✅ Authorization code grant supported
- ✅ PKCE S256 supported
- ✅ JWKS valid structure with keys array
- ✅ Dynamic Client Registration disabled (security best practice)
Advisory Skips (3):
- ⏭️ JWKS key metadata fields (Better Auth limitation - not critical)
- ⏭️ CORS headers on JWKS endpoint (not needed for server-side validation)
- ⏭️ Manual token verification (requires browser-based testing)
Run locally:
npx tsx tests/oauth-validation.test.ts- Start the auth server:
pnpm dev- Ensure database is running:
# Server uses Neon in production, postgres-js locally
# Check DATABASE_URL in .env.local- Seed test data:
pnpm run seed:setuppnpm test-allThis runs:
- All API tests (OAuth, tenant, edge cases, confidential client, default org)
- OAuth compliance validation
# OAuth flows
node tests/test-oauth-flows.js
# Tenant claims
node tests/test-tenant-claims.js
# Edge cases
node tests/test-edge-cases.js
# Tenant edge cases
node tests/test-tenant-edge-cases.js
# Confidential client
node tests/test-confidential-client.js
# Default organization
node tests/test-default-organization.js
# OAuth compliance
npx tsx tests/oauth-validation.test.tsSet in .github/workflows/test-auth.yml:
env:
NODE_ENV: test
DISABLE_EMAIL_VERIFICATION: true # Skip email verification for automated tests
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/auth_test
BETTER_AUTH_URL: http://localhost:3001
BETTER_AUTH_SECRET: test-secret-key-for-ci-onlyCI uses PostgreSQL 16 service container:
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: auth_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5CI automatically creates admin user via Better Auth signup API:
curl -X POST http://localhost:3001/api/auth/sign-up/email \
-H "Content-Type: application/json" \
-d '{
"email": "admin@robolearn.io",
"password": "admin@robolearn.io",
"name": "Admin User"
}'Password Requirements:
- Must be 8+ characters
- Not in HaveIBeenPwned breach database
- Better Auth validates against compromised passwords
Symptom:
❌ PKCE Flow: FAIL - Sign-in failed: 401 {"code":"INVALID_EMAIL_OR_PASSWORD"}
Cause: Password hash mismatch between manual hashing and Better Auth's internal format
Solution: Always use Better Auth's signup API to create test users (as done in CI workflow)
Symptom:
ERR_PNPM_OUTDATED_LOCKFILE Cannot install with "frozen-lockfile"
Solution:
pnpm install
git add pnpm-lock.yaml
git commit -m "fix: Update lockfile"Symptom:
❌ FAIL: User not in default organization!
Email: undefined
Organizations: []
Solution: Profile API must query member table and return flattened response with organizationIds array
Symptom:
❌ JWKS: key[0] has required fields - Missing fields in key 0
Solution: These are Better Auth limitations (missing optional JWKS metadata). Marked as SKIP (advisory) in compliance tests.
Symptom:
❌ Sign-in failed: 403 {"code":"EMAIL_NOT_VERIFIED"}
Solution: Set DISABLE_EMAIL_VERIFICATION=true in test environment
- View CI logs:
gh run list --branch <branch-name> --limit 5
gh run view <run-id> --log- Check specific test failure:
gh run view <run-id> --log | grep -A 20 "❌\|FAIL"- Download test results artifact:
gh run download <run-id>
cat test-results/test-results.json | jq '.'- Run failed tests locally:
# Reproduce CI environment
NODE_ENV=test DISABLE_EMAIL_VERIFICATION=true pnpm dev
# In another terminal
node tests/<test-file>.jsCI also runs security checks:
# Audit dependencies for vulnerabilities
pnpm audit
# Check for outdated dependencies
pnpm outdatedNote: These may show warnings but won't fail the build unless critical vulnerabilities are found.
Test results are saved to:
- CI: Artifact
test-results(downloadable from GitHub Actions) - Local:
tests/test-results.json
Example:
{
"timestamp": "2025-12-02T10:24:44.116Z",
"environment": "http://localhost:3001",
"summary": {
"passed": 9,
"failed": 0,
"skipped": 3,
"total": 12
},
"results": [...]
}// tests/test-my-feature.js
const AUTH_URL = "http://localhost:3001";
async function testMyFeature() {
console.log("Testing my feature...");
// Your test logic
const response = await fetch(`${AUTH_URL}/api/my-endpoint`);
if (response.ok) {
console.log("✅ PASS: My feature works");
process.exit(0);
} else {
console.log("❌ FAIL: My feature failed");
process.exit(1);
}
}
testMyFeature();Update package.json:
{
"scripts": {
"test-api": "node tests/test-oauth-flows.js && node tests/test-my-feature.js && ..."
}
}For special test requirements, modify .github/workflows/test-auth.yml.
- ✅ Use unique test users: Generate unique emails with timestamps to avoid conflicts
- ✅ Clean up after tests: Tests should be idempotent (can run multiple times)
- ✅ Test error cases: Don't just test happy paths
- ✅ Use Better Auth APIs: Don't manually hash passwords or create database records
- ✅ Clear assertions: Print expected vs actual values on failure
- ✅ Fast feedback: Keep test suite under 3 minutes
- ✅ Isolated environment: Each CI run uses fresh database
- ✅ No secrets in logs: Use environment variables for sensitive data
- ✅ Retry on flaky tests: Network timeouts should not fail builds
- ✅ Fail fast: Stop on first critical failure to save CI minutes
Better Auth's JWKS endpoint is missing some optional OAuth 2.1 metadata fields:
- Missing
usefield (should be "sig") - Missing optional fields like
x5c,x5t
Impact: Low - OAuth flows work correctly, just missing metadata for advanced use cases
Workaround: Tests mark these as SKIP (advisory) rather than FAIL
JWKS endpoint doesn't include CORS headers.
Impact: None for server-side OAuth flows (our primary use case)
Workaround: If browser-side JWKS validation needed, add CORS middleware
- Review security audit warnings
- Update outdated dependencies (if no breaking changes)
- Check for Better Auth updates
- Review test coverage (aim for 90%+ of OAuth flows)
- Update test data if schema changes
- Benchmark test suite performance (should stay under 3 minutes)
- Run full test suite locally
- Verify CI passing on main branch
- Check test-results.json for any skipped tests that should be fixed
- Review ADRs for any test-related decisions
- Environment Variables - Required env vars for tests
- Multi-Tenancy - Understanding tenant_id and organization_ids
- ADR 001: Standalone Architecture - Why we use Better Auth
If tests fail unexpectedly:
- Check Troubleshooting section above
- Review recent commits for breaking changes
- Compare CI logs with local test output
- Check Better Auth changelog for breaking changes
- Open an issue with:
- CI run ID
- Test failure logs
- Local reproduction steps