Skip to content

Commit

Permalink
add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jchartrand committed May 30, 2024
1 parent 9f9a59b commit 7a12caf
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 44 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Transaction Manager Service _(@digitalcredentials/transaction-manager-service)_

[![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/transaction-service/main.yml?branch=main)](https://github.com/digitalcredentials/transaction-service/actions?query=workflow%3A%22Node.js+CI%22)

[![Coverage Status](https://coveralls.io/repos/github/digitalcredentials/transaction-service/badge.svg?branch=jc-add-healthz)](https://coveralls.io/github/digitalcredentials/transaction-service?branch=jc-add-healthz)

> Express app for managing the transactions used in [VC-API exchanges](https://w3c-ccg.github.io/vc-api/#initiate-exchange).
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"husky": "^9.0.11",
"lint-staged": "^15.2.5",
"mocha": "^10.2.0",
"nock": "^13.5.4",
"nodemon": "^2.0.21",
"prettier": "3.2.5",
"supertest": "^6.3.3"
Expand Down
16 changes: 5 additions & 11 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import express from 'express'
import logger from 'morgan'
import cors from 'cors'
import axios from 'axios'
import assert from 'node:assert/strict'

import {
initializeTransactionManager,
setupExchange,
Expand All @@ -10,7 +12,6 @@ import {
} from './transactionManager.js'
import { getDataForExchangeSetupPost } from './test-fixtures/testData.js'
import { getSignedDIDAuth } from './didAuth.js'
import TransactionException from './TransactionException.js'

export async function build() {
await initializeTransactionManager()
Expand All @@ -30,6 +31,7 @@ export async function build() {
const baseURL = `${req.protocol}://${req.headers.host}`
const testData = getDataForExchangeSetupPost('test', baseURL)
const exchangeURL = `${baseURL}/exchange`

try {
const response = await axios.post(exchangeURL, testData)
const { data: walletQuerys } = response
Expand All @@ -40,15 +42,8 @@ export async function build() {
const didAuth = await getSignedDIDAuth('did:ex:223234', challenge)
const { data } = await axios.post(requestURI, didAuth)
const { tenantName, vc: unSignedVC } = data
if (
!tenantName === 'test' ||
!unSignedVC.name === 'A Simply Wonderful Course'
) {
throw new TransactionException(
503,
'transaction-service healthz failed'
)
}
assert.equal(tenantName, 'test')
assert.ok(unSignedVC.issuer)
} catch (e) {
console.log(`exception in healthz: ${JSON.stringify(e)}`)
return res.status(503).json({
Expand Down Expand Up @@ -118,7 +113,6 @@ export async function build() {
)
return res.json(data)
} catch (error) {
console.log(error)
return res.status(error.code || 500).json(error)
}
})
Expand Down
207 changes: 206 additions & 1 deletion src/app.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { expect } from 'chai'
import request from 'supertest'
import crypto from 'crypto'
import { build } from './app.js'
import { getDataForExchangeSetupPost } from './test-fixtures/testData.js'
import { getSignedDIDAuth } from './didAuth.js'
import {
clearKeyv,
initializeTransactionManager
} from './transactionManager.js'
import TransactionException from './TransactionException.js'
const tempKeyvFile = process.env.PERSIST_TO_FILE
let app

describe('api', function () {
Expand Down Expand Up @@ -47,6 +50,109 @@ describe('api', function () {
expect(response.body)
expect(response.body.length).to.eql(testData.data.length)
})

it('returns error if missing exchangeHost', async function () {
const testData = getDataForExchangeSetupPost('test')
delete testData.exchangeHost
const response = await request(app).post('/exchange').send(testData)
const body = response.body
expect(response.header['content-type']).to.have.string('json')
expect(response.status).to.eql(400)
expect(body.code).to.eql(400)
expect(body.message).to.eql(
'Incomplete exchange data - you must provide an exchangeHost'
)
})

it('returns error if missing tenantName', async function () {
const testData = getDataForExchangeSetupPost('test')
delete testData.tenantName
const response = await request(app).post('/exchange').send(testData)
const body = response.body
expect(response.header['content-type']).to.have.string('json')
expect(response.status).to.eql(400)
expect(body.code).to.eql(400)
expect(body.message).to.eql(
'Incomplete exchange data - you must provide a tenant name'
)
})

it('returns error if missing vc or subjectData', async function () {
const testData = getDataForExchangeSetupPost('test')
delete testData.data[0].vc
const response = await request(app).post('/exchange').send(testData)
const body = response.body
expect(response.header['content-type']).to.have.string('json')
expect(response.status).to.eql(400)
expect(body.code).to.eql(400)
expect(body.message).to.eql(
'Incomplete exchange data - you must provide either a vc or subjectData'
)
})

it('returns error if missing batchId with subjectData', async function () {
const testData = getDataForExchangeSetupPost('test')
delete testData.data[0].vc
testData.data[0].subjectData = { hello: 'trouble' }
const response = await request(app).post('/exchange').send(testData)
const body = response.body
expect(response.header['content-type']).to.have.string('json')
expect(response.status).to.eql(400)
expect(body.code).to.eql(400)
expect(body.message).to.eql(
'Incomplete exchange data - if you provide subjectData, you must also provide a batchId'
)
})

it('returns error if missing retrievalId', async function () {
const testData = getDataForExchangeSetupPost('test')
delete testData.data[0].retrievalId
const response = await request(app).post('/exchange').send(testData)
const body = response.body
expect(response.header['content-type']).to.have.string('json')
expect(response.status).to.eql(400)
expect(body.code).to.eql(400)
expect(body.message).to.eql(
"Incomplete exchange data - every submitted record must have it's own retrievalId."
)
})
})

describe('keyv', function () {
before(async function () {
clearKeyv()
delete process.env.PERSIST_TO_FILE
app = await build()
})

after(async function () {
process.env.PERSIST_TO_FILE = tempKeyvFile
})

it('uses in-memory keyv', function (done) {
request(app)
.post('/exchange')
.expect('Content-Type', /json/)
.expect(400, done)
})
})

describe('POST /exchange/exchangeId/transactionId', function () {
it('returns 404 if invalid', function (done) {
request(app)
.post('/exchange/234/123')
.expect('Content-Type', /json/)
.expect(404, done)
})
})

describe('POST /exchange/exchangeId', function () {
it('returns 404 if invalid', function (done) {
request(app)
.post('/exchange/234')
.expect('Content-Type', /json/)
.expect(404, done)
})
})

describe('GET /healthz', function () {
Expand All @@ -61,7 +167,7 @@ describe('api', function () {
})
})

it('returns 503 if not healthy', async function () {
it('returns 503 if internal error', async function () {
// we delete the keyv store to force an error
clearKeyv()
const response = await request(app).get('/healthz')
Expand All @@ -84,4 +190,103 @@ describe('api', function () {
expect(obj.stack).to.eql(stack)
})
})

describe('POST /exchange - direct', function () {
it('does the direct exchange', async function () {
const { path, challenge } = await doSetupWithDirectDeepLink(app)
const didAuth = await getSignedDIDAuth('did:ex:223234', challenge)
const exchangeResponse = await request(app).post(path).send(didAuth)
verifyReturnedData(exchangeResponse)
})

it('returns error for bad didAuth', async function () {
const { path } = await doSetupWithDirectDeepLink(app)
// use a different challenge than was issued
const didAuth = await getSignedDIDAuth('did:ex:223234', 'badChallenge')
const exchangeResponse = await request(app).post(path).send(didAuth)
expect(exchangeResponse.header['content-type']).to.have.string('json')
expect(exchangeResponse.status).to.eql(401)
expect(exchangeResponse.body)

const responseErrorObject = exchangeResponse.body
expect(responseErrorObject.code).to.eql(401)
expect(responseErrorObject.message).to.eql('Invalid DIDAuth.')
})

it('does the vpr exchange', async function () {
const walletQuery = await doSetup(app)
const url = walletQuery.vprDeepLink

// Step 2. mimics what the wallet would do when opened by deeplink
// which is to parse the deeplink and call the exchange initiation endpoint
const parsedDeepLink = new URL(url)
const inititationURI = parsedDeepLink.searchParams.get('vc_request_url')

// strip out the host because we are using supertest
const initiationURIPath = new URL(inititationURI).pathname

const initiationResponse = await request(app).post(initiationURIPath)
expect(initiationResponse.header['content-type']).to.have.string('json')
expect(initiationResponse.status).to.eql(200)
expect(initiationResponse.body)

const vpr = initiationResponse.body

// Step 3. mimics what the wallet does once it's got the VPR
const challenge = vpr.verifiablePresentationRequest.challenge // the challenge that the exchange service generated
const continuationURI =
vpr.verifiablePresentationRequest.interact.service.find(
(service) => service.type === 'UnmediatedPresentationService2021'
).serviceEndpoint
// strip out the host because we are using supertest
const continuationURIPath = new URL(continuationURI).pathname
const randomId = `did:ex:${crypto.randomUUID()}`
const didAuth = await getSignedDIDAuth(randomId, challenge)

const continuationResponse = await request(app)
.post(continuationURIPath)
.send(didAuth)

verifyReturnedData(continuationResponse)
})
})
})

const doSetup = async (app) => {
const testData = getDataForExchangeSetupPost('test')
const response = await request(app).post('/exchange').send(testData)

expect(response.header['content-type']).to.have.string('json')
expect(response.status).to.eql(200)
expect(response.body)
expect(response.body.length).to.eql(testData.data.length)

const walletQuerys = response.body
const walletQuery = walletQuerys.find((q) => q.retrievalId === 'someId')
return walletQuery
}

const doSetupWithDirectDeepLink = async (app) => {
const walletQuery = await doSetup(app)
const url = walletQuery.directDeepLink

const parsedDeepLink = new URL(url)
const requestURI = parsedDeepLink.searchParams.get('vc_request_url') //should be http://localhost:4004/exchange?challenge=VOclS8ZiMs&auth_type=bearer
// here we need to pull out just the path
// since we are calling the endpoint via
// supertest
const path = new URL(requestURI).pathname
const challenge = parsedDeepLink.searchParams.get('challenge') // the challenge that the exchange service generated
return { path, challenge }
}

const verifyReturnedData = (exchangeResponse) => {
expect(exchangeResponse.header['content-type']).to.have.string('json')
expect(exchangeResponse.status).to.eql(200)
expect(exchangeResponse.body)

const storedData = exchangeResponse.body
expect(storedData.vc.issuer.id).to.exist
expect(storedData.tenantName).to.eql('test')
expect(storedData.retrievalId).to.eql('someId')
}
9 changes: 6 additions & 3 deletions src/test-fixtures/testData.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import testVC from './testVC.js';
import testVC from './testVC.js'

const getDataForExchangeSetupPost = (tenantName, exchangeHost = 'http://localhost:4005') => {
const getDataForExchangeSetupPost = (
tenantName,
exchangeHost = 'http://localhost:4005'
) => {
const fakeData = {
tenantName,
exchangeHost,
data: [
{ vc: testVC, retrievalId: 'someId', },
{ vc: testVC, retrievalId: 'someId' },
{ vc: testVC, retrievalId: 'blah' }
]
}
Expand Down
Loading

0 comments on commit 7a12caf

Please sign in to comment.