Testing
Grant uses Vitest for all testing across the monorepo.
Quick Start
pnpm test # Watch mode
pnpm test:run # Single run
pnpm test:coverage # With coverage reportTest Structure
apps/api/tests/
├── unit/
│ ├── graphql/ # Field selection, custom scalars
│ ├── i18n/ # i18n error mapper translationKey, helpers
│ │ ├── error-mapper.translationKey.test.ts
│ │ └── helpers.test.ts
│ ├── middleware/ # Rate limiting, request-logging middleware logic
│ │ ├── rate-limit.middleware.test.ts
│ │ └── request-logging.middleware.test.ts
│ ├── handlers/ # Handler optional requestLogger, project OAuth
│ │ ├── auth.handler.project-oauth.test.ts
│ │ ├── auth.handler.request-logger.test.ts
│ │ └── project-oauth.handler.test.ts
│ ├── services/ # Audit service tenant scoping
│ └── jobs/ # Tenant job context validation
├── integration/
│ ├── i18n.integration.test.ts # REST error body includes translationKey and localized error
│ ├── project-oauth.integration.test.ts # Project OAuth authorize, email request, email callback
│ ├── rate-limit.integration.test.ts # HTTP-level rate limit tests
│ ├── observability.integration.test.ts # Metrics endpoint, telemetry/analytics/tracing adapters
│ └── request-logging.integration.test.ts # Request-scoped logger and requestId in log payload
└── e2e/
├── flows.e2e.test.ts # Full flow: register → login → org → invite → project
├── observability.e2e.test.ts # GET /metrics against real API (metrics enabled in E2E stack)
├── scenarios/
│ ├── multi-tenant.e2e.test.ts # Cross-tenant isolation
│ ├── negative-rbac.e2e.test.ts # Authorization boundaries
│ ├── negative-auth.e2e.test.ts # Authentication rejection
│ ├── project-apps.e2e.test.ts # Project app CRUD (create, list, update, delete) via GraphQL
│ ├── project-oauth.e2e.test.ts # Project OAuth authorize, email request, email callback
│ └── user-onboarding.e2e.test.ts # User onboarding flow
└── compliance/
├── soc2-access-control.e2e.test.ts
├── soc2-audit.e2e.test.ts
├── hipaa-phi.e2e.test.ts
└── gdpr.e2e.test.tsConfiguration
| App | Environment | Config |
|---|---|---|
| API | node | apps/api/vitest.config.ts — uses vite-tsconfig-paths for @/ imports |
| Web | jsdom | apps/web/vitest.config.ts — component testing with DOM |
Security and Compliance Tests
E2E tests create real tenant contexts (users, organizations, projects, tokens) and issue actual HTTP requests. Each test follows a positive/negative pattern:
- Positive — verify authorized users can perform operations within their scope
- Negative — verify unauthorized users (other tenants, unauthenticated) get 403/404
Test Suites
| Suite | File | Compliance |
|---|---|---|
| Multi-tenant isolation | multi-tenant.e2e.test.ts | SOC 2 CC6.1, ISO 27001 A.8.3, HIPAA 164.312(a)(1) |
| Negative RBAC | negative-rbac.e2e.test.ts | SOC 2 CC6.1–CC6.2, ISO 27001 A.5.15 |
| Negative authentication | negative-auth.e2e.test.ts | SOC 2 CC6.1, CC6.8 |
| SOC 2 access control | soc2-access-control.e2e.test.ts | CC6.1–CC6.3, CC6.6 |
| SOC 2 audit | soc2-audit.e2e.test.ts | CC7.2–CC7.3 |
| HIPAA | hipaa-phi.e2e.test.ts | 164.312(a–e) |
| GDPR | gdpr.e2e.test.ts | Articles 15, 17, 20, 25, 32 |
Running Security Tests
cd apps/api
# All E2E tests (requires running API and database)
pnpm test tests/e2e/
# Isolation scenarios only
pnpm test tests/e2e/scenarios/multi-tenant.e2e.test.ts
# Compliance suites only
pnpm test tests/e2e/compliance/Rate Limit Testing
Rate limiting is tested at two levels:
- Unit (
tests/unit/middleware/) — middleware logic in isolation with mocked config and store - Integration (
tests/integration/) — HTTP-level via supertest against a minimal Express app
The integration suite includes optional benchmark reporting (duration, req/s). Suppress with BENCHMARK_REPORT=0.
Observability Testing
Observability is covered at two levels:
- Integration (
observability.integration.test.ts) — Metrics endpoint (GET /metrics with mocked config), telemetry adapter (sendLognoop when provider is none), analytics adapter (trackEventnoop when disabled), and tracing shutdown. No real server or external backends. - E2E (
observability.e2e.test.ts) — GET /metrics against the real API container. The E2E stack enables metrics (METRICS_ENABLED=trueindocker-compose.e2e.yml); telemetry and analytics are set to noop/disabled. Full E2E for log-push or analytics would require a test backend (e.g. mock HTTP receiver).
Request logging: Unit tests (request-logging.middleware.test.ts) assert requestId and request-scoped logger on req, and the completion log payload on res.finish. Handler unit test (auth.handler.request-logger.test.ts) asserts that when requestLogger is passed, the handler uses it for error logs. Integration test (request-logging.integration.test.ts) uses a minimal Express app with the middleware and one route that calls getRequestLogger(req).info(...); it asserts the log payload includes requestId and the event message.
Project OAuth testing
Project OAuth (authorize, email magic link, callback) is covered at three levels:
- Unit (
tests/unit/handlers/project-oauth.handler.test.ts,auth.handler.project-oauth.test.ts) — Handler logic in isolation with mocked services, cache, grant, GitHub OAuth, and email. Asserts:initiateProjectAuthorize(NotFound, BadRequest for redirect_uri/provider, 302 URL for github vs email),requestProjectEmailMagicLink(validation, cache set, email send),handleProjectCallback(GitHub: state validation, user resolution, token signing),handleProjectCallbackEmailFlow(token/state validation, user resolution),resolveUserIdFromGithubForProjectandresolveUserIdFromEmailForProject(find by provider/email, link or create user). - Integration (
tests/integration/project-oauth.integration.test.ts) — Minimal Express app with auth routes and realProjectOAuthHandlerbacked by in-memory cache and mocked dependencies. Asserts: GET authorize → 302 with Location (GitHub or email entry URL), POST email/request → 202, GET callback with token/state (payload injected in cache) → 302 with Location fragment containingaccess_token. Redirects are asserted without following (Supertest does not follow redirects by default). - E2E (
tests/e2e/scenarios/project-oauth.e2e.test.ts) — Real API, DB, and Redis. Reuses project-app setup (org, project, project app withenabledProviders). Asserts: authorize 302 for github and email, email/request 202, then Redis helper (tests/e2e/helpers/redis-e2e.ts) reads the one-time token from E2E Redis (key patterngrant:oauth:oauth:project-email-token:*), and GET callback with that token and state → 302 withaccess_tokenin fragment. The user must be inproject_users(DB helperaddProjectUserForE2eindb-tokens.ts).
Redirect handling: Supertest does not follow redirects by default; assert on res.status === 302 and res.headers.location. No need to follow the redirect to validate the flow.
E2E Redis: Set E2E_REDIS_HOST, E2E_REDIS_PORT (default 6380), and E2E_REDIS_PASSWORD if your E2E Redis is not at localhost:6380 with password grant_redis_password (see docker-compose.e2e.yml).
GitHub callback E2E: Full GitHub OAuth flow (user authorizes in browser) is not automated in E2E; it is covered by unit and integration tests with mocked GitHub exchange. To run a full GitHub flow E2E, use a test GitHub OAuth app or a mock OAuth server (see project OAuth testing strategy).
i18n Testing
i18n correctness is covered at three levels:
- Unit (
tests/unit/i18n/) —mapDomainToHttpassigns the correcttranslationKey(and optionaltranslationParams) for each domain exception (NotFoundError, ValidationError, TokenExpiredError, etc.). Helpers unit tests asserttranslateError,t,getLocale, andtranslateStaticwith mockedreq.i18nandgetFixedT. - Integration (
tests/integration/i18n.integration.test.ts) — Minimal Express app with mock i18n middleware and the real error handler; requests that throwAuthenticationErrororNotFoundErrorreturn 401/404 withtranslationKeyand localizederrorin the JSON body. Asserts thatAccept-Languageinfluences the localized message. - E2E —
negative-auth.e2e.test.tsasserts that every 4xx error response includes atranslationKeystring matching^errors\.(and auth/validation/conflict segments where applicable).flows.e2e.test.tsincludes a short "Error response i18n" check: unauthenticatedGET /api/mereturns 401 withtranslationKeyanderror.
Coverage Goals
| Category | Target |
|---|---|
| Unit (utilities, business logic) | > 90% |
| Integration (API endpoints) | > 80% |
| Component (UI) | > 70% |
| Overall | > 80% |
Related:
- Development Guide — Project structure and workflow
- Security Audit — Dependency vulnerability scanning