diff --git a/.github/workflows/e2e-app-smoke.yml b/.github/workflows/e2e-app-smoke.yml new file mode 100644 index 000000000..a09d6c683 --- /dev/null +++ b/.github/workflows/e2e-app-smoke.yml @@ -0,0 +1,40 @@ +name: E2E - App Smoke +run-name: ${{ format('E2E - App Smoke - {0}', github.ref_name) }} + +on: + workflow_dispatch: + push: + paths: + - "server/src/**" + - "server/test/app-smoke.e2e-spec.ts" + - "server/src/db/schema/**" + - "packages/types/src/**" + - "scripts/test/**" + - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" + pull_request: + paths: + - "server/src/**" + - "server/test/app-smoke.e2e-spec.ts" + - "server/src/db/schema/**" + - "packages/types/src/**" + - "scripts/test/**" + - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + checks: write + +jobs: + app-smoke: + uses: ./.github/workflows/e2e-runner.yml + with: + suite-id: app-smoke + check-name: E2E - App Smoke + artifact-name: e2e-app-smoke-report + job-timeout-minutes: 30 diff --git a/.github/workflows/e2e-auth-recovery-oidc-logout.yml b/.github/workflows/e2e-auth-recovery-oidc-logout.yml new file mode 100644 index 000000000..34980d486 --- /dev/null +++ b/.github/workflows/e2e-auth-recovery-oidc-logout.yml @@ -0,0 +1,48 @@ +name: E2E - Auth Recovery OIDC Logout +run-name: ${{ format('E2E - Auth Recovery OIDC Logout - {0}', github.ref_name) }} + +on: + workflow_dispatch: + push: + paths: + - "server/src/modules/auth/**" + - "server/src/modules/user/**" + - "server/src/modules/email/**" + - "server/src/modules/metadata/**" + - "server/src/db/schema/**" + - "server/test/auth-recovery-oidc-logout.e2e-spec.ts" + - "server/test/e2e/app-harness.ts" + - "packages/types/src/auth.ts" + - "scripts/test/**" + - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" + pull_request: + paths: + - "server/src/modules/auth/**" + - "server/src/modules/user/**" + - "server/src/modules/email/**" + - "server/src/modules/metadata/**" + - "server/src/db/schema/**" + - "server/test/auth-recovery-oidc-logout.e2e-spec.ts" + - "server/test/e2e/app-harness.ts" + - "packages/types/src/auth.ts" + - "scripts/test/**" + - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + checks: write + +jobs: + auth-recovery-oidc-logout: + uses: ./.github/workflows/e2e-runner.yml + with: + suite-id: auth-recovery-oidc-logout + check-name: E2E - Auth Recovery OIDC Logout + artifact-name: e2e-auth-recovery-oidc-logout-report + job-timeout-minutes: 50 diff --git a/.github/workflows/e2e-auth-session-security.yml b/.github/workflows/e2e-auth-session-security.yml new file mode 100644 index 000000000..3eda256da --- /dev/null +++ b/.github/workflows/e2e-auth-session-security.yml @@ -0,0 +1,54 @@ +name: E2E - Auth Session Security +run-name: ${{ format('E2E - Auth Session Security - {0}', github.ref_name) }} + +on: + workflow_dispatch: + push: + paths: + - "server/src/modules/auth/**" + - "server/src/modules/user/**" + - "server/src/modules/role/**" + - "server/src/common/guards/**" + - "server/src/common/decorators/**" + - "server/src/common/types/**" + - "server/src/db/schema/**" + - "server/test/auth-session-security.e2e-spec.ts" + - "server/test/e2e/app-harness.ts" + - "packages/types/src/auth.ts" + - "packages/types/src/permissions.ts" + - "scripts/test/**" + - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" + pull_request: + paths: + - "server/src/modules/auth/**" + - "server/src/modules/user/**" + - "server/src/modules/role/**" + - "server/src/common/guards/**" + - "server/src/common/decorators/**" + - "server/src/common/types/**" + - "server/src/db/schema/**" + - "server/test/auth-session-security.e2e-spec.ts" + - "server/test/e2e/app-harness.ts" + - "packages/types/src/auth.ts" + - "packages/types/src/permissions.ts" + - "scripts/test/**" + - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + checks: write + +jobs: + auth-session-security: + uses: ./.github/workflows/e2e-runner.yml + with: + suite-id: auth-session-security + check-name: E2E - Auth Session Security + artifact-name: e2e-auth-session-security-report + job-timeout-minutes: 50 diff --git a/.github/workflows/e2e-authorization-matrix.yml b/.github/workflows/e2e-authorization-matrix.yml new file mode 100644 index 000000000..6a7c5f38a --- /dev/null +++ b/.github/workflows/e2e-authorization-matrix.yml @@ -0,0 +1,42 @@ +name: E2E - Authorization Matrix +run-name: ${{ format('E2E - Authorization Matrix - {0}', github.ref_name) }} + +on: + workflow_dispatch: + push: + paths: + - "server/src/**" + - "server/src/db/schema/**" + - "server/test/authorization-matrix.e2e-spec.ts" + - "server/test/e2e/authorization-matrix/**" + - "packages/types/src/**" + - "scripts/test/**" + - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" + pull_request: + paths: + - "server/src/**" + - "server/src/db/schema/**" + - "server/test/authorization-matrix.e2e-spec.ts" + - "server/test/e2e/authorization-matrix/**" + - "packages/types/src/**" + - "scripts/test/**" + - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + checks: write + +jobs: + authorization-matrix: + uses: ./.github/workflows/e2e-runner.yml + with: + suite-id: authorization-matrix + check-name: E2E - Authorization Matrix + artifact-name: e2e-authorization-matrix-report + job-timeout-minutes: 60 diff --git a/.github/workflows/e2e-email-lifecycle.yml b/.github/workflows/e2e-email-lifecycle.yml index 02337dd30..4f2573fe3 100644 --- a/.github/workflows/e2e-email-lifecycle.yml +++ b/.github/workflows/e2e-email-lifecycle.yml @@ -1,5 +1,5 @@ -name: Email Lifecycle E2E -run-name: ${{ format('Email Lifecycle E2E - {0}', github.ref_name) }} +name: E2E - Email Lifecycle +run-name: ${{ format('E2E - Email Lifecycle - {0}', github.ref_name) }} on: workflow_dispatch: @@ -61,10 +61,10 @@ permissions: checks: write jobs: - email-lifecycle-e2e: + email-lifecycle: uses: ./.github/workflows/e2e-runner.yml with: suite-id: email-lifecycle - check-name: Server Email Lifecycle E2E - artifact-name: email-lifecycle-e2e-report + check-name: E2E - Email Lifecycle + artifact-name: e2e-email-lifecycle-report job-timeout-minutes: 45 diff --git a/.github/workflows/e2e-metadata-write.yml b/.github/workflows/e2e-metadata-write.yml index ceeddcdc6..0f3103f95 100644 --- a/.github/workflows/e2e-metadata-write.yml +++ b/.github/workflows/e2e-metadata-write.yml @@ -1,11 +1,11 @@ -name: Metadata Write E2E -run-name: ${{ format('Metadata Write E2E - {0}', github.ref_name) }} +name: E2E - Metadata Write +run-name: ${{ format('E2E - Metadata Write - {0}', github.ref_name) }} on: workflow_dispatch: inputs: vitest-args: - description: "Optional vitest args (examples: --shard=1/2, --testNamePattern=\"concurrency hardening\")" + description: 'Optional vitest args (examples: --shard=1/2, --testNamePattern="concurrency hardening")' required: false default: "" type: string @@ -45,11 +45,11 @@ permissions: checks: write jobs: - metadata-write-e2e: + metadata-write: uses: ./.github/workflows/e2e-runner.yml with: suite-id: metadata-write - check-name: Server Metadata Write E2E - artifact-name: metadata-write-e2e-report + check-name: E2E - Metadata Write + artifact-name: e2e-metadata-write-report job-timeout-minutes: 60 vitest-args: ${{ github.event_name == 'workflow_dispatch' && inputs.vitest-args || '' }} diff --git a/.github/workflows/e2e-opds-auth-catalog.yml b/.github/workflows/e2e-opds-auth-catalog.yml index 5478fd06b..0610db3ea 100644 --- a/.github/workflows/e2e-opds-auth-catalog.yml +++ b/.github/workflows/e2e-opds-auth-catalog.yml @@ -1,5 +1,5 @@ -name: OPDS Auth Catalog E2E -run-name: ${{ format('OPDS Auth Catalog E2E - {0}', github.ref_name) }} +name: E2E - OPDS Auth Catalog +run-name: ${{ format('E2E - OPDS Auth Catalog - {0}', github.ref_name) }} on: workflow_dispatch: @@ -47,10 +47,10 @@ permissions: checks: write jobs: - opds-auth-catalog-e2e: + opds-auth-catalog: uses: ./.github/workflows/e2e-runner.yml with: suite-id: opds-auth-catalog - check-name: Server OPDS Auth Catalog E2E - artifact-name: opds-auth-catalog-e2e-report + check-name: E2E - OPDS Auth Catalog + artifact-name: e2e-opds-auth-catalog-report job-timeout-minutes: 45 diff --git a/.github/workflows/e2e-reader-state-isolation.yml b/.github/workflows/e2e-reader-state-isolation.yml index 43a65e49e..2d0e2b15b 100644 --- a/.github/workflows/e2e-reader-state-isolation.yml +++ b/.github/workflows/e2e-reader-state-isolation.yml @@ -1,5 +1,5 @@ -name: Reader State Isolation E2E -run-name: ${{ format('Reader State Isolation E2E - {0}', github.ref_name) }} +name: E2E - Reader State Isolation +run-name: ${{ format('E2E - Reader State Isolation - {0}', github.ref_name) }} on: workflow_dispatch: @@ -47,10 +47,10 @@ permissions: checks: write jobs: - reader-state-isolation-e2e: + reader-state-isolation: uses: ./.github/workflows/e2e-runner.yml with: suite-id: reader-state-isolation - check-name: Server Reader State Isolation E2E - artifact-name: reader-state-isolation-e2e-report + check-name: E2E - Reader State Isolation + artifact-name: e2e-reader-state-isolation-report job-timeout-minutes: 45 diff --git a/.github/workflows/e2e-runner.yml b/.github/workflows/e2e-runner.yml index 6ef494f94..d1d9b0830 100644 --- a/.github/workflows/e2e-runner.yml +++ b/.github/workflows/e2e-runner.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: suite-id: - description: "E2E suite id from scripts/test/e2e-suites.mjs (e.g. scanner)" + description: "E2E suite id from scripts/test/e2e-suites.mjs (e.g. scanner-scenarios)" required: true type: string check-name: @@ -72,9 +72,9 @@ jobs: shell: bash run: | if [ -n "${{ inputs.vitest-args }}" ]; then - pnpm run test:e2e -- ${{ inputs.suite-id }} ${{ inputs.vitest-args }} + pnpm run e2e:run -- ${{ inputs.suite-id }} ${{ inputs.vitest-args }} else - pnpm run test:e2e -- ${{ inputs.suite-id }} + pnpm run e2e:run -- ${{ inputs.suite-id }} fi - name: Publish test report annotations diff --git a/.github/workflows/e2e-scanner-file-ops.yml b/.github/workflows/e2e-scanner-file-operations.yml similarity index 51% rename from .github/workflows/e2e-scanner-file-ops.yml rename to .github/workflows/e2e-scanner-file-operations.yml index 07234e6de..6e6682042 100644 --- a/.github/workflows/e2e-scanner-file-ops.yml +++ b/.github/workflows/e2e-scanner-file-operations.yml @@ -1,5 +1,5 @@ -name: Scanner File Ops E2E -run-name: ${{ format('Scanner File Ops E2E - {0}', github.ref_name) }} +name: E2E - Scanner File Operations +run-name: ${{ format('E2E - Scanner File Operations - {0}', github.ref_name) }} on: workflow_dispatch: @@ -8,19 +8,21 @@ on: push: paths: - "server/src/modules/scanner/**" - - "server/test/scanner*" - - "server/test/e2e/**" + - "server/test/scanner-file-operations.e2e-spec.ts" + - "server/test/e2e/scanner/**" - "server/src/db/schema/**" - "scripts/test/**" - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" pull_request: paths: - "server/src/modules/scanner/**" - - "server/test/scanner*" - - "server/test/e2e/**" + - "server/test/scanner-file-operations.e2e-spec.ts" + - "server/test/e2e/scanner/**" - "server/src/db/schema/**" - "scripts/test/**" - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -31,10 +33,10 @@ permissions: checks: write jobs: - scanner-file-ops-e2e: + scanner-file-operations: uses: ./.github/workflows/e2e-runner.yml with: - suite-id: scanner-file-ops - check-name: Server Scanner File Ops E2E - artifact-name: scanner-file-ops-e2e-report + suite-id: scanner-file-operations + check-name: E2E - Scanner File Operations + artifact-name: e2e-scanner-file-operations-report job-timeout-minutes: 60 diff --git a/.github/workflows/e2e-scanner.yml b/.github/workflows/e2e-scanner-scenarios.yml similarity index 51% rename from .github/workflows/e2e-scanner.yml rename to .github/workflows/e2e-scanner-scenarios.yml index ccd6f15a3..ab3b2e386 100644 --- a/.github/workflows/e2e-scanner.yml +++ b/.github/workflows/e2e-scanner-scenarios.yml @@ -1,24 +1,26 @@ -name: Scanner E2E -run-name: ${{ format('Scanner E2E - {0}', github.ref_name) }} +name: E2E - Scanner Scenarios +run-name: ${{ format('E2E - Scanner Scenarios - {0}', github.ref_name) }} on: workflow_dispatch: push: paths: - "server/src/modules/scanner/**" - - "server/test/scanner*" - - "server/test/e2e/**" + - "server/test/scanner-scenarios.e2e-spec.ts" + - "server/test/e2e/scanner/**" - "server/src/db/schema/**" - "scripts/test/**" - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" pull_request: paths: - "server/src/modules/scanner/**" - - "server/test/scanner*" - - "server/test/e2e/**" + - "server/test/scanner-scenarios.e2e-spec.ts" + - "server/test/e2e/scanner/**" - "server/src/db/schema/**" - "scripts/test/**" - "scripts/db/**" + - ".github/workflows/e2e-runner.yml" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -29,10 +31,10 @@ permissions: checks: write jobs: - scanner-e2e: + scanner-scenarios: uses: ./.github/workflows/e2e-runner.yml with: - suite-id: scanner - check-name: Server Scanner E2E - artifact-name: scanner-e2e-report + suite-id: scanner-scenarios + check-name: E2E - Scanner Scenarios + artifact-name: e2e-scanner-scenarios-report job-timeout-minutes: 45 diff --git a/.github/workflows/e2e-staging-ingest-finalize.yml b/.github/workflows/e2e-staging-ingest-finalize.yml index 78e7f2533..f6da56711 100644 --- a/.github/workflows/e2e-staging-ingest-finalize.yml +++ b/.github/workflows/e2e-staging-ingest-finalize.yml @@ -1,5 +1,5 @@ -name: Staging Ingest Finalize E2E -run-name: ${{ format('Staging Ingest Finalize E2E - {0}', github.ref_name) }} +name: E2E - Staging Ingest Finalize +run-name: ${{ format('E2E - Staging Ingest Finalize - {0}', github.ref_name) }} on: workflow_dispatch: @@ -33,10 +33,10 @@ permissions: checks: write jobs: - staging-ingest-finalize-e2e: + staging-ingest-finalize: uses: ./.github/workflows/e2e-runner.yml with: suite-id: staging-ingest-finalize - check-name: Server Staging Ingest Finalize E2E - artifact-name: staging-ingest-finalize-e2e-report + check-name: E2E - Staging Ingest Finalize + artifact-name: e2e-staging-ingest-finalize-report job-timeout-minutes: 45 diff --git a/.github/workflows/e2e-users-admin-lifecycle.yml b/.github/workflows/e2e-users-admin-lifecycle.yml index 0e8025d08..2ab7d0c1b 100644 --- a/.github/workflows/e2e-users-admin-lifecycle.yml +++ b/.github/workflows/e2e-users-admin-lifecycle.yml @@ -1,5 +1,5 @@ -name: Users Admin Lifecycle E2E -run-name: ${{ format('Users Admin Lifecycle E2E - {0}', github.ref_name) }} +name: E2E - Users Admin Lifecycle +run-name: ${{ format('E2E - Users Admin Lifecycle - {0}', github.ref_name) }} on: workflow_dispatch: @@ -43,10 +43,10 @@ permissions: checks: write jobs: - users-admin-lifecycle-e2e: + users-admin-lifecycle: uses: ./.github/workflows/e2e-runner.yml with: suite-id: users-admin-lifecycle - check-name: Server Users Admin Lifecycle E2E - artifact-name: users-admin-lifecycle-e2e-report + check-name: E2E - Users Admin Lifecycle + artifact-name: e2e-users-admin-lifecycle-report job-timeout-minutes: 50 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c239174f8..38dd648b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -118,9 +118,9 @@ pnpm verify # run all checks before pushing Other useful commands: ```bash -pnpm quick # faster checks while coding +pnpm verify:fast # lint + typecheck + tests (pre-push gate) pnpm db:reset # wipe and reseed local database -pnpm doctor # environment health check +pnpm e2e:list # list available e2e suite ids ``` --- @@ -205,9 +205,9 @@ Use a **draft PR** early if you want feedback on direction before the work is do The project has two automatic gates you should be aware of: - **pre-commit:** runs `lint-staged` on your staged files. ESLint and Prettier are applied automatically. You do not need to format or lint manually before committing. -- **pre-push:** runs `pnpm quick` (lint + typecheck + tests). If this fails, the push is blocked. Fix the errors before pushing. +- **pre-push:** runs `pnpm verify:fast` (lint + typecheck + tests). If this fails, the push is blocked. Fix the errors before pushing. -`pnpm quick` is a fast check but does not include the e2e smoke suite. Before opening a PR, run the full gate: +`pnpm verify:fast` is a fast check but does not include the `app-smoke` e2e suite. Before opening a PR, run the full gate: ```bash pnpm verify @@ -216,7 +216,7 @@ pnpm verify Before marking your PR ready for review: - [ ] Linked to an approved issue -- [ ] `pnpm verify` passes locally (includes e2e smoke, stricter than pre-push) +- [ ] `pnpm verify` passes locally (includes `app-smoke`, stricter than pre-push) - [ ] You ran the full stack and manually verified the change works - [ ] UI changes include a screenshot or screen recording - [ ] Tests are included per the expectations above diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index bebf57fba..509d864e8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -114,21 +114,21 @@ Opens Drizzle Studio in your browser for browsing and editing tables directly. pnpm run test # all tests (server + client) pnpm run test:server # server unit tests only pnpm run test:client # client unit tests only -pnpm run test:e2e -- all # run all e2e suites sequentially -pnpm run test:e2e -- smoke # e2e smoke suite (default DB) -pnpm run test:e2e -- scanner # scanner e2e suite (dedicated projectx_e2e DB) -pnpm run test:e2e -- scanner-file-ops # scanner file operation e2e suite (dedicated projectx_e2e DB) -pnpm run test:e2e -- staging-ingest-finalize # staging ingest/finalize e2e suite (dedicated projectx_e2e DB) -pnpm run test:e2e -- opds-auth-catalog # OPDS auth/catalog e2e suite (dedicated projectx_e2e DB) -pnpm run test:e2e -- reader-state-isolation # reader state isolation e2e suite (dedicated projectx_e2e DB) -pnpm run test:e2e -- email-lifecycle # email lifecycle e2e suite (dedicated projectx_e2e DB) -pnpm run test:e2e -- users-admin-lifecycle # users admin lifecycle e2e suite (dedicated projectx_e2e DB) -pnpm run test:e2e:all # alias for "pnpm run test:e2e -- all" -pnpm run test:e2e:opds-auth-catalog # alias for "pnpm run test:e2e -- opds-auth-catalog" -pnpm run test:e2e:reader-state-isolation # alias for "pnpm run test:e2e -- reader-state-isolation" -pnpm run test:e2e:email-lifecycle # alias for "pnpm run test:e2e -- email-lifecycle" -pnpm run test:e2e:users-admin-lifecycle # alias for "pnpm run test:e2e -- users-admin-lifecycle" -pnpm run test:e2e:list # list all supported e2e suite ids +pnpm run e2e:run -- all # run all e2e suites sequentially +pnpm run e2e:run -- app-smoke # app smoke suite (default DB) +pnpm run e2e:run -- scanner-scenarios # scanner scenario suite (dedicated projectx_e2e DB) +pnpm run e2e:run -- scanner-file-operations # scanner file operation suite (dedicated projectx_e2e DB) +pnpm run e2e:run -- auth-session-security # auth session security suite (dedicated projectx_e2e DB) +pnpm run e2e:run -- auth-recovery-oidc-logout # auth recovery and OIDC logout suite (dedicated projectx_e2e DB) +pnpm run e2e:run -- staging-ingest-finalize # staging ingest/finalize suite (dedicated projectx_e2e DB) +pnpm run e2e:run -- metadata-write # metadata write suite (dedicated projectx_e2e DB) +pnpm run e2e:run -- authorization-matrix # authorization matrix suite (dedicated projectx_e2e DB) +pnpm run e2e:run -- opds-auth-catalog # OPDS auth/catalog suite (dedicated projectx_e2e DB) +pnpm run e2e:run -- reader-state-isolation # reader state isolation suite (dedicated projectx_e2e DB) +pnpm run e2e:run -- email-lifecycle # email lifecycle suite (dedicated projectx_e2e DB) +pnpm run e2e:run -- users-admin-lifecycle # users admin lifecycle suite (dedicated projectx_e2e DB) +pnpm run e2e:all # shortcut for running all suites +pnpm run e2e:list # list supported suite ids ``` Watch mode while working on a specific area: @@ -140,95 +140,63 @@ cd server && pnpm test:watch Run a focused scanner scenario: ```bash -pnpm run test:e2e -- scanner --testNamePattern=book-per-folder-disc-folder-flattening +pnpm run e2e:run -- scanner-scenarios --testNamePattern=book-per-folder-disc-folder-flattening ``` ### E2E runner architecture - All suite metadata lives in `scripts/test/e2e-suites.mjs` (suite id, spec file, JUnit output, DB prep mode). -- Local and CI use the same command: `pnpm run test:e2e -- `. -- Use `all` as a composite suite id to run every configured suite in order. -- Dedicated-db suites auto-start local PostgreSQL when `CI != true`, then reset and migrate the e2e database through `pnpm run db:prepare:e2e`. +- Local and CI use the same command: `pnpm run e2e:run -- `. +- Use `all` as a composite suite id to run every configured suite in order, or use `pnpm run e2e:all`. +- Dedicated-db suites auto-start local PostgreSQL when `CI != true`, then reset and migrate the e2e database through `pnpm run e2e:db:prepare`. - JUnit outputs are written to `test-results/server/`. -### Scanner e2e details +### E2E suite details -- `pnpm run test:e2e -- scanner` prepares and migrates a dedicated e2e database (`projectx_e2e`) on each run. -- Local runs auto-start PostgreSQL with `docker-compose.dev.yml` if needed. -- Results are written to `test-results/server/`: - - `scanner-e2e-junit.xml` - - `scanner-e2e-scenarios.json` +All suites write JUnit XML to `test-results/server/`. Dedicated-db suites also auto-start PostgreSQL locally (when needed) and use the `projectx_e2e` database. -### Scanner file operation e2e details - -- `pnpm run test:e2e -- scanner-file-ops` prepares and migrates the same dedicated e2e database (`projectx_e2e`) before running. -- Local runs auto-start PostgreSQL with `docker-compose.dev.yml` if needed. -- Results are written to `test-results/server/`: - - `scanner-file-ops-e2e-junit.xml` - - `scanner-file-ops-e2e-scenarios.json` - -### Staging ingest/finalize e2e details - -- `pnpm run test:e2e -- staging-ingest-finalize` prepares and migrates the dedicated e2e database (`projectx_e2e`) before running. -- Local runs auto-start PostgreSQL with `docker-compose.dev.yml` if needed. -- Results are written to `test-results/server/`: - - `staging-ingest-finalize-e2e-junit.xml` - - `staging-ingest-finalize-e2e-scenarios.json` - -### OPDS auth/catalog e2e details - -- `pnpm run test:e2e -- opds-auth-catalog` prepares and migrates the dedicated e2e database (`projectx_e2e`) before running. -- Local runs auto-start PostgreSQL with `docker-compose.dev.yml` if needed. -- Results are written to `test-results/server/`: - - `opds-auth-catalog-e2e-junit.xml` - - `opds-auth-catalog-e2e-scenarios.json` - -### Reader state isolation e2e details - -- `pnpm run test:e2e -- reader-state-isolation` prepares and migrates the dedicated e2e database (`projectx_e2e`) before running. -- Local runs auto-start PostgreSQL with `docker-compose.dev.yml` if needed. -- Results are written to `test-results/server/`: - - `reader-state-isolation-e2e-junit.xml` - - `reader-state-isolation-e2e-scenarios.json` - -### Email lifecycle e2e details - -- `pnpm run test:e2e -- email-lifecycle` prepares and migrates the dedicated e2e database (`projectx_e2e`) before running. -- Local runs auto-start PostgreSQL with `docker-compose.dev.yml` if needed. -- Results are written to `test-results/server/`: - - `email-lifecycle-e2e-junit.xml` - - `email-lifecycle-e2e-scenarios.json` - -### Users admin lifecycle e2e details - -- `pnpm run test:e2e -- users-admin-lifecycle` prepares and migrates the dedicated e2e database (`projectx_e2e`) before running. -- Local runs auto-start PostgreSQL with `docker-compose.dev.yml` if needed. -- Results are written to `test-results/server/`: - - `users-admin-lifecycle-e2e-junit.xml` - - `users-admin-lifecycle-e2e-scenarios.json` +| Suite id | DB mode | JUnit output | Extra artifacts | +| --------------------------- | ------------ | ----------------------------------------- | -------------------------------------------- | +| `app-smoke` | default-db | `app-smoke-e2e-junit.xml` | none | +| `scanner-scenarios` | dedicated-db | `scanner-scenarios-e2e-junit.xml` | `scanner-scenarios-e2e-scenarios.json` | +| `scanner-file-operations` | dedicated-db | `scanner-file-operations-e2e-junit.xml` | `scanner-file-operations-e2e-scenarios.json` | +| `auth-session-security` | dedicated-db | `auth-session-security-e2e-junit.xml` | none | +| `auth-recovery-oidc-logout` | dedicated-db | `auth-recovery-oidc-logout-e2e-junit.xml` | none | +| `staging-ingest-finalize` | dedicated-db | `staging-ingest-finalize-e2e-junit.xml` | `staging-ingest-finalize-e2e-scenarios.json` | +| `metadata-write` | dedicated-db | `metadata-write-e2e-junit.xml` | `metadata-write-e2e-scenarios.json` | +| `authorization-matrix` | dedicated-db | `authorization-matrix-e2e-junit.xml` | none | +| `opds-auth-catalog` | dedicated-db | `opds-auth-catalog-e2e-junit.xml` | `opds-auth-catalog-e2e-scenarios.json` | +| `reader-state-isolation` | dedicated-db | `reader-state-isolation-e2e-junit.xml` | `reader-state-isolation-e2e-scenarios.json` | +| `email-lifecycle` | dedicated-db | `email-lifecycle-e2e-junit.xml` | `email-lifecycle-e2e-scenarios.json` | +| `users-admin-lifecycle` | dedicated-db | `users-admin-lifecycle-e2e-junit.xml` | `users-admin-lifecycle-e2e-scenarios.json` | ### E2E in CI (how to trigger) All suite workflows call the reusable `E2E Runner (reusable)` workflow, which runs: ```bash -pnpm run test:e2e -- +pnpm run e2e:run -- ``` -| Workflow | Suite id | Triggered by | -| --------------------------- | ------------------------- | ------------------------------------------------------------------------------------- | -| Scanner E2E | `scanner` | `workflow_dispatch`, `push` (scanner-related paths), `pull_request` (same paths) | -| Scanner File Ops E2E | `scanner-file-ops` | `workflow_dispatch`, nightly schedule (`0 4 * * *`), `push`, `pull_request` | -| Staging Ingest Finalize E2E | `staging-ingest-finalize` | `workflow_dispatch`, `push` (staging-related paths), `pull_request` (same paths) | -| OPDS Auth Catalog E2E | `opds-auth-catalog` | `workflow_dispatch`, `push` (opds-related paths), `pull_request` (same paths) | -| Reader State Isolation E2E | `reader-state-isolation` | `workflow_dispatch`, `push` (reader-state-related paths), `pull_request` (same paths) | -| Email Lifecycle E2E | `email-lifecycle` | `workflow_dispatch`, `push` (email-related paths), `pull_request` (same paths) | -| Users Admin Lifecycle E2E | `users-admin-lifecycle` | `workflow_dispatch`, `push` (users/auth-related paths), `pull_request` (same paths) | +| Workflow name | Suite id | Triggered by | +| --------------------------------- | --------------------------- | --------------------------------------------------------------------------- | +| `E2E - App Smoke` | `app-smoke` | `workflow_dispatch`, `push`, `pull_request` | +| `E2E - Scanner Scenarios` | `scanner-scenarios` | `workflow_dispatch`, `push`, `pull_request` | +| `E2E - Scanner File Operations` | `scanner-file-operations` | `workflow_dispatch`, nightly schedule (`0 4 * * *`), `push`, `pull_request` | +| `E2E - Auth Session Security` | `auth-session-security` | `workflow_dispatch`, `push`, `pull_request` | +| `E2E - Auth Recovery OIDC Logout` | `auth-recovery-oidc-logout` | `workflow_dispatch`, `push`, `pull_request` | +| `E2E - Staging Ingest Finalize` | `staging-ingest-finalize` | `workflow_dispatch`, `push`, `pull_request` | +| `E2E - Metadata Write` | `metadata-write` | `workflow_dispatch`, `push`, `pull_request` | +| `E2E - Authorization Matrix` | `authorization-matrix` | `workflow_dispatch`, `push`, `pull_request` | +| `E2E - OPDS Auth Catalog` | `opds-auth-catalog` | `workflow_dispatch`, `push`, `pull_request` | +| `E2E - Reader State Isolation` | `reader-state-isolation` | `workflow_dispatch`, `push`, `pull_request` | +| `E2E - Email Lifecycle` | `email-lifecycle` | `workflow_dispatch`, `push`, `pull_request` | +| `E2E - Users Admin Lifecycle` | `users-admin-lifecycle` | `workflow_dispatch`, `push`, `pull_request` | Manual trigger steps: 1. Open GitHub Actions. -2. Select **Scanner E2E**, **Scanner File Ops E2E**, **Staging Ingest Finalize E2E**, **OPDS Auth Catalog E2E**, **Reader State Isolation E2E**, **Email Lifecycle E2E**, or **Users Admin Lifecycle E2E**. +2. Select any `E2E - ...` workflow for the suite you want to run. 3. Click **Run workflow**. Each run uploads `test-results/server/` as an artifact and publishes JUnit annotations from `test-results/server/*-e2e-junit.xml`. @@ -241,7 +209,7 @@ Run before pushing: ```bash pnpm run verify:fast # lint + typecheck + tests (same as pre-push hook) -pnpm run verify # above + e2e smoke (run before opening a PR) +pnpm run verify # above + e2e app-smoke suite (run before opening a PR) ``` Individual checks: diff --git a/package.json b/package.json index 4050b1cbd..cba79adbc 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "dev:client": "cd client && npm run dev -- --host", "dev:server": "cd server && npm run start:dev", "verify:fast": "pnpm run lint:check && pnpm run typecheck && pnpm run test", - "verify": "pnpm run lint:check && pnpm run typecheck && pnpm run test && pnpm run test:e2e -- smoke", - "verify:strict": "pnpm run format:check && pnpm run lint:check && pnpm run typecheck:full && pnpm run test && pnpm run test:e2e -- smoke", + "verify": "pnpm run lint:check && pnpm run typecheck && pnpm run test && pnpm run e2e:run -- app-smoke", + "verify:strict": "pnpm run format:check && pnpm run lint:check && pnpm run typecheck:full && pnpm run test && pnpm run e2e:run -- app-smoke", "build:client": "pnpm --filter client build", "build:server": "pnpm --filter server build", "typecheck": "pnpm run typecheck:server && pnpm run typecheck:client", @@ -22,16 +22,9 @@ "test": "pnpm run test:server && pnpm run test:client", "test:server": "pnpm --filter server test", "test:client": "pnpm --filter client test:unit --run", - "test:e2e": "node ./scripts/test/run-e2e-suite.mjs", - "test:e2e:all": "node ./scripts/test/run-e2e-suite.mjs all", - "test:e2e:metadata-write": "node ./scripts/test/run-e2e-suite.mjs metadata-write", - "test:e2e:opds-auth-catalog": "node ./scripts/test/run-e2e-suite.mjs opds-auth-catalog", - "test:e2e:metadata-write:atomicity": "node ./scripts/test/run-e2e-suite.mjs metadata-write --testNamePattern=\"atomicity failpoints\"", - "test:e2e:metadata-write:concurrency": "node ./scripts/test/run-e2e-suite.mjs metadata-write --testNamePattern=\"concurrency hardening\"", - "test:e2e:reader-state-isolation": "node ./scripts/test/run-e2e-suite.mjs reader-state-isolation", - "test:e2e:email-lifecycle": "node ./scripts/test/run-e2e-suite.mjs email-lifecycle", - "test:e2e:users-admin-lifecycle": "node ./scripts/test/run-e2e-suite.mjs users-admin-lifecycle", - "test:e2e:list": "node ./scripts/test/run-e2e-suite.mjs --list", + "e2e:run": "node ./scripts/test/run-e2e-suite.mjs", + "e2e:all": "node ./scripts/test/run-e2e-suite.mjs all", + "e2e:list": "node ./scripts/test/run-e2e-suite.mjs --list", "coverage": "pnpm run coverage:server && pnpm run coverage:client", "coverage:server": "pnpm --filter server test:cov", "coverage:client": "pnpm --filter client test:cov --run", @@ -41,7 +34,8 @@ "db:migrate": "pnpm --filter server db:migrate", "db:reset": "bash ./scripts/db/reset.sh", "db:seed": "bash ./scripts/db/seed.sh", - "db:prepare:e2e": "bash ./scripts/db/prepare-e2e.sh", + "e2e:db:prepare": "bash ./scripts/db/prepare-e2e.sh", + "e2e:db:migrate": "pnpm --filter server e2e:db:migrate", "prepare": "husky" }, "engines": { diff --git a/scripts/db/prepare-e2e.sh b/scripts/db/prepare-e2e.sh index 64337aaf4..2deeaad77 100644 --- a/scripts/db/prepare-e2e.sh +++ b/scripts/db/prepare-e2e.sh @@ -7,7 +7,7 @@ cd "$ROOT_DIR" E2E_DATABASE_URL="${E2E_DATABASE_URL:-postgres://projectx:projectx@localhost:5432/projectx_e2e}" echo "Preparing dedicated e2e database..." -DATABASE_URL="$E2E_DATABASE_URL" pnpm --filter server db:prepare:e2e +DATABASE_URL="$E2E_DATABASE_URL" pnpm --filter server e2e:db:prepare echo "Applying migrations to e2e database..." -DATABASE_URL="$E2E_DATABASE_URL" pnpm --filter server db:migrate +DATABASE_URL="$E2E_DATABASE_URL" pnpm --filter server e2e:db:migrate diff --git a/scripts/test/e2e-suites.mjs b/scripts/test/e2e-suites.mjs index 20a42fa7d..8ed2b3904 100644 --- a/scripts/test/e2e-suites.mjs +++ b/scripts/test/e2e-suites.mjs @@ -1,27 +1,27 @@ const TEST_RESULTS_DIR = "../test-results/server"; export const E2E_SUITES = Object.freeze({ - smoke: { - id: "smoke", - description: "Server smoke suite", - vitestTarget: "test/app.e2e-spec.ts", - junitOutput: `${TEST_RESULTS_DIR}/smoke-e2e-junit.xml`, + "app-smoke": { + id: "app-smoke", + description: "App smoke suite", + vitestTarget: "test/app-smoke.e2e-spec.ts", + junitOutput: `${TEST_RESULTS_DIR}/app-smoke-e2e-junit.xml`, prepareDedicatedDatabase: false, useDedicatedDatabase: false, }, - scanner: { - id: "scanner", + "scanner-scenarios": { + id: "scanner-scenarios", description: "Scanner scenario matrix", - vitestTarget: "test/scanner.e2e-spec.ts", - junitOutput: `${TEST_RESULTS_DIR}/scanner-e2e-junit.xml`, + vitestTarget: "test/scanner-scenarios.e2e-spec.ts", + junitOutput: `${TEST_RESULTS_DIR}/scanner-scenarios-e2e-junit.xml`, prepareDedicatedDatabase: true, useDedicatedDatabase: true, }, - "scanner-file-ops": { - id: "scanner-file-ops", + "scanner-file-operations": { + id: "scanner-file-operations", description: "Scanner file operation matrix", - vitestTarget: "test/scanner-file-ops.e2e-spec.ts", - junitOutput: `${TEST_RESULTS_DIR}/scanner-file-ops-e2e-junit.xml`, + vitestTarget: "test/scanner-file-operations.e2e-spec.ts", + junitOutput: `${TEST_RESULTS_DIR}/scanner-file-operations-e2e-junit.xml`, prepareDedicatedDatabase: true, useDedicatedDatabase: true, }, diff --git a/scripts/test/run-e2e-suite.mjs b/scripts/test/run-e2e-suite.mjs index 62db1ff05..9cf948cee 100644 --- a/scripts/test/run-e2e-suite.mjs +++ b/scripts/test/run-e2e-suite.mjs @@ -28,17 +28,17 @@ function trimLeadingSeparator(args) { function printUsage() { const usage = [ "Usage:", - " pnpm run test:e2e -- [vitest args...]", + " pnpm run e2e:run -- [vitest args...]", "", "Examples:", - " pnpm run test:e2e -- smoke", - " pnpm run test:e2e -- all", - " pnpm run test:e2e -- metadata-write", - " pnpm run test:e2e -- metadata-write --testNamePattern=\"concurrency hardening\"", - " pnpm run test:e2e -- metadata-write --shard=1/2", - " pnpm run test:e2e -- scanner --testNamePattern=book-per-folder-disc-folder-flattening", - " pnpm run test:e2e -- --list", - " pnpm run test:e2e:list", + " pnpm run e2e:run -- app-smoke", + " pnpm run e2e:run -- all", + " pnpm run e2e:run -- metadata-write", + ' pnpm run e2e:run -- metadata-write --testNamePattern="concurrency hardening"', + " pnpm run e2e:run -- metadata-write --shard=1/2", + " pnpm run e2e:run -- scanner-scenarios --testNamePattern=book-per-folder-disc-folder-flattening", + " pnpm run e2e:run -- --list", + " pnpm run e2e:list", ]; console.log(usage.join("\n")); } @@ -122,7 +122,7 @@ async function main() { if (suite.prepareDedicatedDatabase) { console.log(`Resetting and migrating dedicated e2e database for suite ${suite.id}...`); - await runCommand("pnpm", ["run", "db:prepare:e2e"], { E2E_DATABASE_URL: e2eDatabaseUrl }); + await runCommand("pnpm", ["run", "e2e:db:prepare"], { E2E_DATABASE_URL: e2eDatabaseUrl }); } const junitOutput = process.env.JUNIT_OUTPUT ?? suite.junitOutput; diff --git a/server/README.md b/server/README.md index 35d0adb1f..2a2f64e99 100644 --- a/server/README.md +++ b/server/README.md @@ -50,16 +50,16 @@ pnpm db:studio # open Drizzle Studio (DB browser) ```bash pnpm test # unit tests pnpm test:watch # unit tests in watch mode -pnpm test:e2e # smoke end-to-end test from server package +pnpm e2e:run # app-smoke end-to-end test from server package pnpm test:cov # unit tests with coverage ``` -For suite-based e2e runs (scanner, scanner-file-ops, auth suites), run from the repository root: +For suite-based e2e runs, run from the repository root: ```bash -pnpm run test:e2e:list -pnpm run test:e2e -- all -pnpm run test:e2e -- scanner +pnpm run e2e:list +pnpm run e2e:all +pnpm run e2e:run -- scanner-scenarios ``` ## Quality diff --git a/server/package.json b/server/package.json index 786d4cbdd..11f100783 100644 --- a/server/package.json +++ b/server/package.json @@ -16,9 +16,9 @@ "start:prod": "node dist/main", "db:generate": "drizzle-kit generate --name", "db:migrate": "drizzle-kit migrate", - "db:migrate:e2e": "DATABASE_URL=postgres://projectx:projectx@localhost:5432/projectx_e2e pnpm db:migrate", + "e2e:db:migrate": "DATABASE_URL=postgres://projectx:projectx@localhost:5432/projectx_e2e pnpm db:migrate", "db:migrate:runtime": "node dist/scripts/migrate.js", - "db:prepare:e2e": "tsx src/scripts/db-prepare-e2e.ts", + "e2e:db:prepare": "tsx src/scripts/db-prepare-e2e.ts", "db:studio": "drizzle-kit studio", "db:seed": "tsx src/scripts/db-seed.ts", "pretype-check": "pnpm --filter @projectx/types build", @@ -26,10 +26,10 @@ "lint": "pnpm run lint:check", "lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\"", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "vitest run --exclude test/scanner.e2e-spec.ts --exclude test/scanner-file-ops.e2e-spec.ts", + "test": "vitest run --exclude test/scanner-scenarios.e2e-spec.ts --exclude test/scanner-file-operations.e2e-spec.ts", "test:watch": "vitest", "test:cov": "vitest run --coverage", - "test:e2e": "vitest run test/app.e2e-spec.ts" + "e2e:run": "vitest run test/app-smoke.e2e-spec.ts" }, "dependencies": { "7z-wasm": "^1.2.0", @@ -84,10 +84,10 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", - "@nestjs/cli": "^11.0.16", - "@nestjs/schematics": "^11.0.9", + "@nestjs/cli": "^11.0.17", + "@nestjs/schematics": "^11.0.10", "@nestjs/testing": "^11.1.17", - "@swc/cli": "^0.8.0", + "@swc/cli": "^0.8.1", "@swc/core": "^1.15.21", "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.6", @@ -112,7 +112,7 @@ "tsconfig-paths": "^4.2.0", "tsx": "^4.21.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.57.2", + "typescript-eslint": "^8.58.0", "vitest": "^4.1.2" } } diff --git a/server/test/app.e2e-spec.ts b/server/test/app-smoke.e2e-spec.ts similarity index 100% rename from server/test/app.e2e-spec.ts rename to server/test/app-smoke.e2e-spec.ts diff --git a/server/test/scanner-file-ops.e2e-spec.ts b/server/test/scanner-file-operations.e2e-spec.ts similarity index 100% rename from server/test/scanner-file-ops.e2e-spec.ts rename to server/test/scanner-file-operations.e2e-spec.ts diff --git a/server/test/scanner.e2e-spec.ts b/server/test/scanner-scenarios.e2e-spec.ts similarity index 100% rename from server/test/scanner.e2e-spec.ts rename to server/test/scanner-scenarios.e2e-spec.ts