Skip to main content

End-to-end journeys

The end-to-end (E2E) suite drives the production build of the app in Chromium against a fresh local Hardhat chain with real v0.3 contracts, two contract sets and the access gate. Wallet prompts are answered by an injected test wallet backed by the node's unlocked development accounts. Only the upload and storage services are replaced by an in-memory stand-in, so no Pinata account, Redis or key is needed.

How the harness is built​

Global setup​

e2e/global-setup.ts runs once before the journeys:

  1. Refuses to start if anything already answers JSON-RPC on port 8545 or HTTP on port 3100 ("Stop it so the E2E run owns a fresh local chain").
  2. Refuses to start if src/contracts/v03/deployment.31337.json or src/contracts/zk/deployment.31337.json already exist, because the run deletes the files it creates and these would belong to someone's own local deployment (assertNoForeignFiles in e2e/support/guards.ts).
  3. Starts npx hardhat node --port 8545 and checks the chain id is 31337 (assertLocalChain refuses any other chain, because the harness signs with publicly known development keys).
  4. Deploys the first v0.3 set with GUARDIAN_ADDRESS set to the guardian account, registers the issuer (seedIssuer) and issues one credential in that set (seedOlderCredential).
  5. Deploys a second set sharing the same issuer registry (ADD_SET=1, ACTIVATE_SET=1, SET_LABEL=next), so new credentials go to the second set while the first stays readable.
  6. Deploys the access gate and runs npm run zk:public.
  7. Builds and starts the app (startApp in e2e/support/app-server.ts).
  8. Writes e2e/.generated/state.json with the chain id, addresses, the older credential id and its registry.

The teardown stops every process and removes the generated build, manifests and state.

The app server​

startApp builds with next build into .next-e2e (so a running dev server is left alone) and serves it with next start -p 3100 -H 127.0.0.1. The environment sets NEXT_PUBLIC_CONTRACT_VERSION=v03, chain 31337, the local RPC for browser and server, NEXT_PUBLIC_START_BLOCK=0, and turns on the optional features with throwaway values that are public on purpose and protect nothing: a status list signer key, an API signer key, one API key (its hash in API_KEYS), cache times of 0 and API_ALLOW_LOCAL_LIMITS=true.

Accounts​

e2e/support/accounts.ts names Hardhat development accounts by role:

NameRole in the journeys
ADMINDeployer and admin of the registries
ISSUERRegistered as "Tribhuvan University"
STUDENTThe holder
GUARDIANHolds the guardian role
NEW_ISSUERThe successor key in the key retirement journey ("Tribhuvan University (new key)")

Fixtures​

e2e/support/fixtures.ts extends Playwright's test:

FixtureWhat it gives a journey
chainReset (automatic)Takes evm_snapshot before the journey and evm_revert after it; fails loudly if the revert does not succeed, so the next journey never starts from dirty state
walletPage(account)A new browser context with the test wallet installed for that account, the upload API mocked, downloads accepted
visitorPage()A new browser context with no wallet at all, only the storage gateway mock, as a verifier would have

The test wallet​

installTestWallet (e2e/support/test-wallet.ts) injects an EIP-1193 window.ethereum (isE2ETestWallet: true). Account requests answer with the one account; every other request is forwarded to the local node, which signs for its unlocked accounts. A signing or sending request for any other account is refused ("The test wallet is ... but ... was requested for ..."). No key is held in the browser or the harness.

Upload and storage stand-in​

mockUploadApi (e2e/support/mock-upload.ts) answers in the browser context:

RouteStand-in behaviour
/api/auth/nonce, /api/auth/verifyA fixed nonce and { ok: true }
/api/uploadPins the uploaded file in a shared in-memory MockStorage; like the real route it refuses anything that is not an encrypted container (PLAINTEXT_NOT_ACCEPTED)
/api/documentsLists or removes copies by document hash, refusing to remove when the copy to keep is unknown (KEEP_NOT_FOUND)
**/ipfs/*Serves back exactly the bytes that were pinned, so a visitor can open what an issuer stored

The real upload route, with Pinata, Redis and the role check, is covered by src/app/api/upload/route.test.ts.

Other helpers​

FilePurpose
e2e/support/journey-steps.tsShared steps and sample documents (for example a diploma for "Asha Rai")
e2e/support/admin.tsAdmin actions the site does not offer: unpauseAsAdmin, replaceIssuerKey
e2e/support/access-members.tsadmitFillerMembers for the deep access group journey
e2e/support/older-set.ts, seed.ts, state.ts, paths.ts, processes.ts, local-chain.tsSeeding, state, paths, process control and chain checks

The helpers have their own Vitest tests: guards.test.ts, local-chain.test.ts, mock-storage.test.ts, test-wallet.test.ts.

Running​

npm ci && npm --prefix blockchain ci
npx playwright install chromium
npm run test:e2e # all journeys
npx playwright test e2e/journeys/issue-and-verify.journey.ts # one file

Ports 8545 and 3100 must be free and the two local manifests must not exist. playwright.config.ts runs one worker (the chain is shared), a 90 second timeout per journey, 15 seconds per assertion, no retries, traces and screenshots kept on failure, and forbidOnly in CI. Reports go to playwright-report/, failure traces to test-results/.

The journeys​

22 files with 38 journeys (counted with grep -cE "^\s*test\(" e2e/journeys/*.ts on 2026-10-24). The descriptions are the journey titles.

FileJourneys
issue-and-verify.journey.tsAn issuer issues a credential of its own type and a visitor with no wallet verifies it from the QR code
revoke-and-reverify.journey.tsAn issuer revokes a credential and a visitor then sees it revoked
dashboard-actions.journey.ts (4)Revoke from the dashboard after confirming; re-issue marks the old one superseded; a revoked credential cannot be re-issued as a replacement; a rejected second step of a re-issue across sets is reminded and finished
delivery.journey.ts (2)Issuing without delivery asks first, and going back lets the issuer tick it; credentials issued with delivery before and after the invitation reach the student once they accept, undelivered ones never do
invite-and-inbox.journey.tsA student accepts an invitation and finds the credential in their inbox with its QR code
manage-student-link.journey.ts (2)An issuer unlinks a wallet and the student no longer sees the credential; an issuer cancels an invitation and the student can no longer accept it
encrypted-documents.journey.ts (2)A document is stored only as ciphertext and the holder and issuer can each open it; a package with the wrong key cannot open it
share-and-replace.journey.tsA holder shares by file and by link, and the issuer's key replacement shuts the old key out
replace-shared-file.journey.tsAn issuer who sent one file to two students replaces a key and is not offered the removal that would break the other copy
selective-disclosure.journey.ts (2)A holder shows only the programme and a visitor verifies it, and a revoke voids it; an earlier credential gets disclosure data from the dashboard
vc-export.journey.ts (2)An issuer downloads a VC, a visitor checks it, a revoke voids it; a VC for another credential is refused
open-badge.journey.ts (3)An Open Badge is checked without a wallet and voided by a revoke; a badge for another credential is refused; a badge with the standard proof is checked and a changed claim fails it
status-lists.journey.tsThe served lists follow suspend, reinstate and revoke, and a visitor checks them against the chain
verification-api.journey.tsA relying party's backend checks a credential through the signed API, before and after a revoke
service-probe.journey.tsThe canary probe passes, reports how fresh the API's view is, and sees a revocation within one run
mobile-wallet.journey.ts (2)A phone QR is checked offline and online and voided by a revoke; a verifier's challenge is signed and cannot be replayed
access-proof.journey.ts (2)A requester proves access with a browser proof and the issuer sees the receipt; a never-admitted requester is refused and nothing is sent
access-proof-depth6.journey.tsA proof for the deepest group the app serves is made in the browser and accepted by the gate
bulk-import.journey.ts (3)Import, stop and carry on without duplicates, then verify; a file with mistakes is refused and nothing is signed; a corrected row is flagged and can replace the earlier credential in the same transaction
contract-sets.journey.ts (3)A credential of the older set is found, named and managed in its own set; a new credential lands in the active set and an unlisted id is found in no set; the dashboard lists and revokes the older set's credential
pause-and-restart.journey.tsA guardian pauses: issuing stops, verification and revocation keep working, the admin restarts
key-retirement.journey.tsAn admin retires an issuer key: old credentials stay valid, the new key takes over, the old key can do nothing

What the suite does not cover​

Pinata, Redis and the real upload route (unit tested instead), MetaMask and other wallet extensions, Sepolia, the v0.2 contract (not deployed by the harness), browsers other than Chromium. These are covered by the Manual checks.

Source files​

  • playwright.config.ts, e2e/global-setup.ts
  • e2e/support/fixtures.ts, test-wallet.ts, mock-upload.ts, mock-storage.ts, app-server.ts, accounts.ts, local-chain.ts, guards.ts, seed.ts, older-set.ts, admin.ts, journey-steps.ts
  • e2e/journeys/*.journey.ts
  • blockchain/scripts/deploy-v03.ts, blockchain/scripts/deploy-access-gate.ts, scripts/zk-public.mjs