An integration test framework should be integrated as part of this project. It should use a full instance of the VCVerifier, with all other components being mocked or test-doubles/implementations being used. It should at least test the following flows.
The following flows need to be tested. They should be tested for case:
- multiple JWT-VCs requested and presented
- One JWT-VC requested and presented
- multiple SD-JWT requested and presented
- One SD-JWT requested and presented
- issuer uses did:key as id
- issuer uses did:web as id
- correctly holder-bound credentials(cnf) presented
- correctly claim-based holder-bound credentials presented
- wrong credentials presented
- credentials without the requested claims presented
- invalid credentials presented
- invalid signed presentation presented
- invalid cnf presented
- invalid claim-based holder binding presented
- (Test)Frontend-Client initiates at /api/v1/authorization
- returns redirect for /api/v2/loginQR
- follows redirect, get QR
- scans QR, starts Cross-Device Flow
- handles authentication request:
- byReference
- byValue
- anwers request
- verifier redirects to application
- applications get JWT
- Client initiates at /api/v1/authorization
- returns redirect to openid-deeplink, to fullfil the same-device flow
- Test-Client follows redirect
- handles authentication request:
- byReference
- byValue
- anwers request
- client follows redirect, get JWT
The integration tests treat the VCVerifier as an opaque HTTP service. The test suite:
- Builds the verifier binary via
go build - Generates a YAML config file at test setup time, pointing to mock HTTP servers
- Launches the binary as a subprocess with
CONFIG_FILE=<path>environment variable - Interacts with it purely over HTTP — no Go imports from any verifier-internal package
- Tears down the process after each test group
This ensures the tests validate the actual shipped artifact and cannot accidentally depend on internal state, unexported functions, or in-process shortcuts. No source code changes to the verifier are required.
The test harness starts lightweight httptest.Server instances before launching the verifier. Their URLs are injected into the generated YAML config.
- TIR (Trusted Issuers Registry) — mock server at a random port, handles
GET /v4/issuers/<did>returningTrustedIssuerJSON or 404. Also handlesGET /v4/issuersfor IsTrustedParticipant calls (returns 200 if DID is trusted, 404 otherwise). - did:web resolution — mock server serving
GET /.well-known/did.jsonfor the did:web issuer test case. Thedid:webDID is derived from the mock server'slocalhost:<port>address. - (Gaia-X and JAdES mocks are not needed for the defined test flows — they can be added later.)
Each test group generates a server.yaml in a temp directory. Service scopes use DCQL (Digital Credentials Query Language) to define which credentials the verifier requests. The verifier embeds the DCQL query as dcql_query in the request object JWT (byValue/byReference modes). The wallet (test client) responds with a vp_token whose format depends on the DCQL query structure.
Example generated config:
server:
port: <free-port>
host: "http://localhost:<free-port>"
templateDir: "views/"
staticDir: "views/static/"
logging:
level: "DEBUG"
jsonLogging: true
logRequests: true
verifier:
did: "did:key:<generated-verifier-did>"
tirAddress: "http://localhost:<tir-mock-port>"
validationMode: "none"
keyAlgorithm: "ES256"
generateKey: true
sessionExpiry: 30
jwtExpiration: 30
supportedModes: ["byValue", "byReference"]
clientIdentification:
id: "did:key:<generated-verifier-did>"
keyPath: "<path-to-generated-pem>"
requestKeyAlgorithm: "ES256"
m2m:
authEnabled: false
configRepo:
services:
- id: "<service-id>"
defaultOidcScope: "<scope>"
authorizationType: "<DEEPLINK|FRONTEND_V2>"
oidcScopes:
<scope>:
credentials:
- type: "<credential-type>"
trustedIssuersLists:
- "http://localhost:<tir-mock-port>"
holderVerification:
enabled: <true|false>
claim: "<claim-path>"
dcql:
credentials:
- id: "<query-id>"
format: "jwt_vc_json"
meta:
vct_values:
- "<credential-type>"
claims:
- path: ["$.vc.credentialSubject.someField"]For SD-JWT credential types, the format field in the DCQL query is dc+sd-jwt instead of jwt_vc_json.
The clientIdentification.keyPath points to a PEM file generated at test setup time (ECDSA P-256 key), which the verifier uses for signing request objects in byValue/byReference modes.
When the verifier's request object contains a dcql_query, the wallet responds with a vp_token that is a JSON map keyed by credential query IDs from the DCQL query. Each value is a VP JWT (or SD-JWT) answering that query:
{
"query-id-1": "<vp-jwt-for-query-1>",
"query-id-2": "<vp-jwt-for-query-2>"
}The verifier's getPresentationFromQuery function parses this map, extracts each VP, and merges all credentials into a single presentation for validation.
For a single credential query, the map has one entry. For multiple credential queries, each query ID maps to its own VP JWT. The test helpers must construct this map format and base64-encode or JSON-encode it as the vp_token form value.
The test helpers create real, cryptographically signed VCs and VPs using the same libraries the verifier depends on (trustbloc, lestrrat-go/jwx). These helpers are in a separate Go module under integration_test/ to avoid polluting the main module's dependencies:
- did:key identities: Generate ECDSA P-256 key pairs, derive
did:keyDIDs usingtrustbloc/did-go/method/keyCreator - JWT-VC signing: Build JWT claims for a VC, sign with
jws.Sign()using the issuer's private key,kidheader set to the issuer's DID key ID - VP signing: Build JWT claims wrapping VC JWTs in the
vpclaim, sign with the holder's private key - SD-JWT: Construct SD-JWT strings (issuer JWT + disclosures + optional key binding JWT) following RFC 9449
- DCQL vp_token map: Build a
map[string]stringmapping DCQL credential query IDs to VP JWT strings, then JSON-encode it for thevp_tokenform value - did:web DID documents: Build a DID document JSON containing the identity's public key, served by the did:web mock server
TestMain (or suite setup)
├── go build -o <tmpdir>/vcverifier .
│
For each test group:
├── Start mock TIR server (httptest.Server)
├── Start mock did:web server if needed (httptest.Server)
├── Generate signing key PEM file
├── Generate server.yaml → <tmpdir>/server.yaml
├── Launch: CONFIG_FILE=<tmpdir>/server.yaml <tmpdir>/vcverifier
├── Wait for health check: poll GET /health until 200 (with timeout)
├── Run test cases against http://localhost:<port>
├── Send SIGTERM to verifier process
└── Clean up temp files and mock servers
The verifier process is started fresh for each test group (not each individual test case) to keep test execution fast. Test groups that need different configurations (e.g., different authorizationType, different holder verification settings) each get their own process.
After launching the subprocess, poll GET /health with a short interval (100ms) and a timeout (10s). If the process exits before becoming healthy, capture stderr for diagnostics.
All integration test files use //go:build integration so they don't run during go test ./.... Run explicitly:
go test -tags integration ./integration_test/... -v -count=1The -count=1 disables test caching since integration tests depend on external processes.
integration_test/
go.mod -- separate Go module (depends on trustbloc, jwx for credential creation)
go.sum
helpers/
identity.go -- TestIdentity struct, GenerateDidKeyIdentity(), GenerateDidWebIdentity()
credentials.go -- CreateJWTVC(), CreateVPToken(), CreateSDJWT(), CreateDCQLResponse()
process.go -- VerifierProcess: build, launch, health-wait, shutdown
tir_mock.go -- Mock TIR httptest.Server returning TrustedIssuer JSON
did_web_mock.go -- Mock did:web httptest.Server serving /.well-known/did.json
config.go -- YAML config generation with DCQL, free port allocation, PEM key file generation
m2m_test.go -- M2M success + failure flow tests (vp_token grant type)
frontend_v2_test.go -- Frontend v2 cross-device flow tests
deeplink_test.go -- Deeplink same-device flow tests
Using a separate go.mod ensures:
- The test helper dependencies (trustbloc for did:key creation, jwx for signing) don't leak into the main module if they diverge
- The integration tests are clearly decoupled from the verifier source
go test ./...from the project root naturally skips them (separate module)
Goal: The build/launch/teardown harness and credential creation helpers that all tests depend on.
helpers/process.go:
BuildVerifier(projectRoot string) (binaryPath string, err error)— runsgo build -o <tmpdir>/vcverifier .in the project rootVerifierProcessstruct: holdscmd *exec.Cmd,Port int,BaseURL string,configDir stringStartVerifier(configYAML string, projectRoot string, binaryPath string) (*VerifierProcess, error)— writes config to temp file, starts binary withCONFIG_FILEenv var, polls/health(*VerifierProcess) Stop()— sends SIGTERM, waits with timeout, kills if needed, cleans temp dirwaitForHealthy(baseURL string, timeout time.Duration) error— pollsGET /healthGetFreePort() (int, error)— binds to:0, reads the assigned port, closes
helpers/config.go:
ConfigBuilderstruct with fluent API for constructing the YAML config:NewConfigBuilder(verifierPort int, tirURL string) *ConfigBuilderWithService(id, scope, authzType string) *ConfigBuilderWithCredential(serviceId, scope, credType, tirURL string) *ConfigBuilderWithHolderVerification(serviceId, scope, credType, claim string) *ConfigBuilderWithDCQL(serviceId, scope string, dcql DCQLConfig) *ConfigBuilderWithSigningKey(keyPath string) *ConfigBuilderBuild() string— returns YAML string
DCQLConfigstruct: mirrors the DCQL YAML structure for config generationCredentialQuerystruct:Id string,Format string,Meta *MetaConfig,Claims []ClaimConfig- Helper:
NewJWTVCQuery(id, credType string) CredentialQuery - Helper:
NewSDJWTQuery(id, vctValue string) CredentialQuery
GenerateSigningKeyPEM(dir string) (keyPath string, err error)— generates ECDSA P-256 key, writes PEM to fileGenerateVerifierDID() (did string, err error)— generates a did:key for the verifier's identity
helpers/identity.go:
TestIdentitystruct:PrivateKey crypto.Signer,PublicKeyJWK jwk.Key,DID string,KeyID stringGenerateDidKeyIdentity() (*TestIdentity, error)— ECDSA P-256 key → did:key DID via trustbloc CreatorGenerateDidWebIdentity(host string) (*TestIdentity, error)— ECDSA P-256 key → did:web DID derived from host
helpers/credentials.go:
CreateJWTVC(issuer *TestIdentity, credType string, subject map[string]interface{}) (string, error)— signed JWT-VCCreateJWTVCWithHolder(issuer *TestIdentity, credType string, subject map[string]interface{}, holderDID string) (string, error)— JWT-VC with claim-based holder binding (adds holder DID into credentialSubject)CreateJWTVCWithCnf(issuer *TestIdentity, credType string, subject map[string]interface{}, holderJWK jwk.Key) (string, error)— JWT-VC withcnfholder binding (addscnf.jwkto the credential)CreateVPToken(holder *TestIdentity, nonce string, audience string, vcJWTs ...string) (string, error)— signed VP JWT wrapping one or more VC JWTsCreateSDJWT(issuer *TestIdentity, vct string, claims map[string]interface{}, disclosedClaims []string) (string, error)— SD-JWT credential stringCreateVPWithSDJWT(holder *TestIdentity, nonce string, audience string, sdJWTs ...string) (string, error)— VP JWT containing SD-JWT credentialsCreateDCQLResponse(queryResponses map[string]string) (string, error)— takes a map of DCQL credential query ID → VP JWT string, JSON-encodes it into thevp_tokenvalue expected by the verifier
helpers/tir_mock.go:
MockTIRstruct: maps DID →TrustedIssuer(struct defined locally in test helpers, mirroring the TIR JSON schema)TrustedIssuerstruct:Did string,Attributes []IssuerAttributeIssuerAttributestruct:Hash string,Body string(base64-encoded JSON of credential config),IssuerType string,Tao string,RootTao stringNewMockTIR(issuers map[string]TrustedIssuer) *httptest.Server— returns running mock- Handles:
GET /v4/issuers/<did>→ 200 with TrustedIssuer JSON, or 404GET /v4/issuers?page=<n>&size=<s>→ paginated list (for IsTrustedParticipant)
BuildIssuerAttribute(credentialType string, claims []string) IssuerAttribute— helper to build properly base64-encoded attribute bodies
helpers/did_web_mock.go:
NewDidWebServer(identity *TestIdentity) *httptest.Server— servesGET /.well-known/did.jsonwith a DID document containing the identity's public key in JWK format
Goal: Test the VP-token-to-JWT exchange via POST /services/:service_id/token (grant_type=vp_token). This exercises the full credential validation pipeline without session management.
File: integration_test/m2m_test.go
Table-driven parameterized tests. Each test case gets a fresh verifier process only if the config differs from the previous one (optimization: group cases that share the same config).
DCQL config per test case: Each test case defines its DCQL query in the service config. For single-credential tests, one CredentialQuery entry. For multi-credential tests, multiple CredentialQuery entries. The test client builds a DCQL response map matching the query IDs.
Test cases:
| Test name | Format | Count | Issuer DID | Holder binding | DCQL query |
|---|---|---|---|---|---|
| One JWT-VC with did:key issuer | JWT-VC | 1 | did:key | none | 1 query: jwt_vc_json, vct CustomerCredential |
| Multiple JWT-VCs with did:key issuer | JWT-VC | 2 | did:key | none | 2 queries: jwt_vc_json, vct TypeA + TypeB |
| One SD-JWT with did:key issuer | SD-JWT | 1 | did:key | none | 1 query: dc+sd-jwt, vct CustomerCredential |
| Multiple SD-JWTs with did:key issuer | SD-JWT | 2 | did:key | none | 2 queries: dc+sd-jwt, vct TypeA + TypeB |
| JWT-VC with did:web issuer | JWT-VC | 1 | did:web | none | 1 query: jwt_vc_json, vct CustomerCredential |
| JWT-VC with cnf holder binding | JWT-VC | 1 | did:key | cnf | 1 query: jwt_vc_json, vct CustomerCredential |
| JWT-VC with claim-based holder binding | JWT-VC | 1 | did:key | claim | 1 query: jwt_vc_json, vct CustomerCredential |
Test pattern (each case):
- Generate issuer + holder identities (did:key or did:web)
- Start mock TIR with trusted issuer entries allowing the credential type
- If did:web: start mock did:web server
- Generate verifier config YAML with DCQL query matching the credential format and type
- Start verifier process
- Create signed VCs (JWT-VC or SD-JWT)
- Create signed VP(s) containing the VCs
- Build DCQL response map:
{"<query-id>": "<vp-jwt>", ...}and JSON-encode it viaCreateDCQLResponse() POST http://localhost:<port>/services/{serviceId}/tokenwith form bodygrant_type=vp_token&vp_token=<dcql-response>&scope=<scope>- Assert HTTP 200, parse JSON response body as
{"token_type":"Bearer","access_token":"...","id_token":"...","expires_in":...} - Verify the returned JWT:
GET http://localhost:<port>/.well-known/jwks, parse JWKS, verify JWT signature, check claims - Stop verifier, close mocks
Goal: Test all failure scenarios for the VP-token exchange.
File: integration_test/m2m_failure_test.go
Test cases:
| Test name | Setup | Expected |
|---|---|---|
| Wrong credential type | DCQL requests TypeA, VP contains TypeB |
400 |
| Missing required claims | VC lacks claims that TIR requires in its attribute body | 400 |
| Untrusted issuer | VC signed by issuer whose DID is not in mock TIR | 400 |
| Invalid VP signature | VP JWT signed with a different key than the holder's | 400 |
| Invalid cnf binding | VC has cnf.jwk for holder A, but VP is signed by holder B | 400 |
| Invalid claim-based holder binding | VC's holder claim contains DID-A, but VP is signed by DID-B | 400 |
Test pattern: Same as Step 2, but the DCQL response map contains VPs with the invalid credentials. Assert non-200 status code and verify the error response body.
Goal: End-to-end test of the frontend v2 cross-device flow, treating the verifier as a black box. Verifies that the dcql_query claim is present in the request object and that DCQL-formatted responses are accepted.
File: integration_test/frontend_v2_test.go
Configure the service with authorizationType: "FRONTEND_V2" and a DCQL query in the generated YAML.
Two sub-tests: byReference and byValue. Use an HTTP client configured with CheckRedirect returning http.ErrUseLastResponse to capture redirects without following them.
Test flow (byReference):
GET /api/v1/authorization?client_id=<svcId>&response_type=code&scope=<scope>&state=<state>&redirect_uri=<uri>&nonce=<nonce>- Assert 302, parse Location header → confirm it points to
/api/v2/loginQR?state=...&client_id=...&redirect_uri=...&scope=...&nonce=...&request_mode=byReference GET /api/v2/loginQR?<params>— returns HTML page containing theopenid4vp://URL- Parse the HTML response to extract the
openid4vp://authentication request URL (regex or string scan for the protocol scheme) - Parse the
openid4vp://URL → extractrequest_uriquery parameter GET /api/v1/request/<id>— fetch the request object (JWT string in response body)- Decode the request object JWT (without verification — it's the verifier's own JWT) to extract
response_uri,state,nonce, anddcql_query - Assert
dcql_queryis present and matches the configured DCQL query structure (correct credential query IDs, format, vct_values) - Create valid VCs matching the DCQL query
- Build DCQL response map keyed by the query IDs from step 7
- Open WebSocket connection to
ws://localhost:<port>/ws?state=<state>(usinggorilla/websocketornhooyr.io/websocket) POST /api/v1/authentication_responsewith form bodystate=<state>&vp_token=<dcql-response>- Assert HTTP 200
- Read WebSocket message — parse JSON to extract
redirectUrlcontaining the authorizationcode POST /tokenwith form bodygrant_type=authorization_code&code=<code>&redirect_uri=<uri>- Assert HTTP 200 with valid JWT in response
Test flow (byValue): Same but the request object JWT is embedded in the openid4vp:// URL query parameter request instead of fetched by reference. Skip step 6; decode the JWT from the URL directly.
Note on the QR/HTML step: Since this is a black-box test, we must work with the HTML response from /api/v2/loginQR. The openid4vp:// URL is embedded in the page for QR code rendering. Extracting it via string matching on the HTML is acceptable for integration tests.
Goal: End-to-end test of the deeplink/same-device flow. Same DCQL verification as Frontend v2 but using the same-device redirect pattern.
File: integration_test/deeplink_test.go
Configure the service with authorizationType: "DEEPLINK" and a DCQL query in the generated YAML.
Two sub-tests: byReference and byValue. The deeplink flow uses byReference by default from the authorization endpoint.
Test flow (byReference):
GET /api/v1/authorization?client_id=<svcId>&response_type=code&scope=<scope>&state=<state>&redirect_uri=<uri>&nonce=<nonce>- Assert 302, parse Location header → confirm it starts with
openid4vp:// - Parse the
openid4vp://URL → extractrequest_uriquery parameter GET /api/v1/request/<id>— fetch request object JWT- Decode JWT → extract
response_uri,state,nonce, anddcql_query - Assert
dcql_queryis present with expected query structure - Create valid VCs matching the DCQL query, build DCQL response map
POST <response_uri>(=http://localhost:<port>/api/v1/authentication_response) with form bodystate=<state>&vp_token=<dcql-response>- Assert 302, parse Location header → extract
codeandstatequery parameters from the redirect URL POST /tokenwith form bodygrant_type=authorization_code&code=<code>&redirect_uri=<uri>- Assert HTTP 200 with valid JWT in response
Test flow (byValue): Same but the openid4vp:// URL from step 2 contains the request object JWT directly in a request parameter. Skip step 4.
Goal: Additional tests not specific to one flow.
File: integration_test/endpoints_test.go
These tests run against a single verifier process with a basic config (including a DCQL query).
GET /.well-known/jwks→ 200, response is valid JWKS JSON with at least one keyGET /services/<id>/.well-known/openid-configuration→ 200, response containsissuer,token_endpoint,jwks_uriGET /health→ 200POST /tokenwithoutgrant_type→ 400POST /tokenwithgrant_type=unsupported→ 400GET /api/v1/authorizationwithoutclient_id→ 400GET /api/v1/authorizationwithoutscope→ 400GET /api/v1/authorizationwithoutstate→ 400
# Build + run all integration tests
go test -tags integration ./integration_test/... -v -count=1
# Run only M2M tests
go test -tags integration ./integration_test/... -v -count=1 -run TestM2M
# Run only deeplink tests
go test -tags integration ./integration_test/... -v -count=1 -run TestDeeplinkThe success/failure credential variations (Step 2 + 3) are tested via the M2M flow — this directly exercises the full credential validation pipeline without session management overhead. The authorization flow tests (Step 4 + 5) additionally verify that:
- The request object JWT contains a
dcql_queryclaim matching the service config - The test client can parse the DCQL query, construct a matching DCQL response map, and complete the flow
All tests use DCQL for credential query configuration. The vp_token is always submitted in the DCQL response map format ({"<query-id>": "<vp-jwt>"}). All tests are true black-box: they only interact with the verifier over HTTP and only depend on its public configuration contract (server.yaml + CONFIG_FILE env var).