From 26c9f957345b61ec9337e03553cc7e8726a8cb9b Mon Sep 17 00:00:00 2001 From: Kayode Ezike Date: Sat, 20 Apr 2024 11:08:29 -0400 Subject: [PATCH] fixes tests and adds back ENABLE_HTTPS_FOR_DEV environment variable --- .dockerignore | 3 +- .env.example | 1 + README.md | 3 +- server.js | 24 ++++++-- src/app.test.js | 100 ++++++++++++++++++++++++--------- src/config.js | 24 ++++---- src/middleware/errorHandler.js | 2 +- src/status.js | 12 ++-- src/test-fixtures/.env.testing | 17 ++++-- src/test-fixtures/fixtures.js | 52 +++++++++++------ src/utils/logger.js | 2 +- 11 files changed, 163 insertions(+), 77 deletions(-) diff --git a/.dockerignore b/.dockerignore index 1453c54..ac16682 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ -**/*.env \ No newline at end of file +**/*.env +node_modules diff --git a/.env.example b/.env.example index da0dcfd..857fa88 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ CRED_STATUS_SERVICE=mongodb CRED_STATUS_DID_SEED=z1AackbUm8U69ohKnihoRRFkXcXJd4Ra1PkAboQ2ZRy1ngB PORT=4008 # default port is 4008 ENABLE_ACCESS_LOGGING=true +ENABLE_HTTPS_FOR_DEV=false ERROR_LOG_FILE=logs/error.log ALL_LOG_FILE=logs/all.log CONSOLE_LOG_LEVEL=silly # default is silly, i.e. log everything - see the README for allowed levels diff --git a/README.md b/README.md index 823a5e2..df27f5f 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ This service provides support for managing credential status in a variety of dat | `CRED_STATUS_DID_SEED` | seed used to deterministically generate DID | string | yes | | `PORT` | HTTP port on which to run the express app | number | no (default: `4008`) | | `ENABLE_ACCESS_LOGGING` | whether to enable access logging (see [Logging](#logging)) | boolean | no (default: `true`) | +| `ENABLE_HTTPS_FOR_DEV` | whether to enable HTTPS in a development instance of the app | boolean | no (default: `true`) | | `ERROR_LOG_FILE` | log file for all errors (see [Logging](#logging)) | string | no | | `ALL_LOG_FILE` | log file for everything (see [Logging](#logging)) | string | no | | `CONSOLE_LOG_LEVEL` | console log level (see [Logging](#logging)) | `error` \| `warn`\| `info` \| `http` \| `verbose` \| `debug` \| `silly` | no (default: `silly`) | @@ -210,7 +211,7 @@ NOTE: CURL can get a bit clunky if you want to experiment more (e.g., by changin ### Revoke -Revocation is fully explained in the Bitstring Status List specification and our implemenations thereof, but effectively, it amounts to POSTing an object to the revocation endpoint, like so: +Revocation and suspension are fully explained in the [Bitstring Status List](https://www.w3.org/TR/vc-bitstring-status-list/) specification and our implemenations thereof, but effectively, it amounts to POSTing an object to the revocation endpoint, like so: ``` {credentialId: '23kdr', credentialStatus: [{type: 'BitstringStatusListCredential', status: 'revoked'}]} diff --git a/server.js b/server.js index c88c242..dc2a5d7 100644 --- a/server.js +++ b/server.js @@ -1,12 +1,26 @@ -import { build } from './src/app.js' -import { getConfig, setConfig } from './src/config.js'; +import { build } from './src/app.js'; +import { getConfig } from './src/config.js'; import http from 'http'; +import https from 'https'; +import logger from './src/utils/logger.js'; const run = async () => { - await setConfig() - const { port } = getConfig(); + const { port, enableHttpsForDev } = getConfig(); const app = await build(); - http.createServer(app).listen(port, () => console.log(`Server running on port ${port}`)); + + if (enableHttpsForDev) { + https + .createServer( + { + key: fs.readFileSync('server-dev-only.key'), + cert: fs.readFileSync('server-dev-only.cert') + }, + app + ).listen(port, () => logger.info(`Server running on port ${port} with https`)); + } else { + http + .createServer(app).listen(port, () => logger.info(`Server running on port ${port} with http`)); + } }; run(); diff --git a/src/app.test.js b/src/app.test.js index 85f2372..bcc7bd8 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -1,13 +1,20 @@ import { expect } from 'chai'; import sinon from 'sinon'; import request from 'supertest'; -import { getUnsignedVC, getUnsignedVCWithStatus, getValidStatusUpdateBody, getInvalidStatusUpdateBody } from './test-fixtures/fixtures.js'; +import { + validCredentialId, + invalidCredentialId, + invalidCredentialIdErrorMessage, + getUnsignedVC, + getUnsignedVCWithStatus, + getValidStatusUpdateBody, + getInvalidStatusUpdateBody +} from './test-fixtures/fixtures.js'; import status from './status.js'; import { build } from './app.js'; const allocateEndpoint = '/credentials/status/allocate'; const updateEndpoint = '/credentials/status'; -const missingCredIdErrorMessage = 'Unable to find credential with given ID'; const emptyStatusManagerStub = {}; describe('api', () => { @@ -20,20 +27,20 @@ describe('api', () => { .get('/'); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(200); - expect(response.body.message).to.eql('status-service-db server status: ok.'); + expect(response.status).to.equal(200); + expect(response.body.message).to.equal('status-service-db server status: ok.'); }); }); - describe('GET /unknown', () => { + describe('GET /unknown/path', () => { it('unknown endpoint returns 404', async () => { await status.initializeStatusManager(emptyStatusManagerStub); const app = await build(); const response = await request(app) - .get('/unknown'); + .get('/unknown/path'); - expect(response.status).to.eql(404); + expect(response.status).to.equal(404); }, 10000); }); @@ -46,13 +53,13 @@ describe('api', () => { .post(allocateEndpoint); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(400); + expect(response.status).to.equal(400); }); it('returns updated credential', async () => { const unsignedVCWithStatus = getUnsignedVCWithStatus(); - const allocateStatus = sinon.fake.returns(unsignedVCWithStatus); - const statusManagerStub = { allocateStatus }; + const allocateSupportedStatuses = sinon.fake.returns(unsignedVCWithStatus); + const statusManagerStub = { allocateSupportedStatuses }; await status.initializeStatusManager(statusManagerStub); const app = await build(); @@ -61,7 +68,7 @@ describe('api', () => { .send(getUnsignedVC()); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(200); + expect(response.status).to.equal(200); expect(response.body).to.eql(unsignedVCWithStatus); }); @@ -76,7 +83,7 @@ describe('api', () => { .send(getUnsignedVCWithStatus()); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(200); + expect(response.status).to.equal(200); expect(response.body).to.eql(getUnsignedVCWithStatus()); }); }); @@ -90,39 +97,78 @@ describe('api', () => { .post(updateEndpoint); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(400); + expect(response.status).to.equal(400); + }); + + it('returns update from revoked credential', async () => { + const revokeCredential = sinon.fake.returns({ + code: 200, + message: 'Credential successfully revoked.' + }); + const statusManagerStub = { revokeCredential }; + await status.initializeStatusManager(statusManagerStub); + const app = await build(); + + const response = await request(app) + .post(updateEndpoint) + .send(getValidStatusUpdateBody(validCredentialId, 'revoked')); + + expect(response.header['content-type']).to.have.string('json'); + expect(response.status).to.equal(200); + expect(response.body.message).to.equal('Credential successfully revoked.'); + }); + + it('returns update from suspended credential', async () => { + const suspendCredential = sinon.fake.returns({ + code: 200, + message: 'Credential successfully suspended.' + }); + const statusManagerStub = { suspendCredential }; + await status.initializeStatusManager(statusManagerStub); + const app = await build(); + + const response = await request(app) + .post(updateEndpoint) + .send(getValidStatusUpdateBody(validCredentialId, 'suspended')); + + expect(response.header['content-type']).to.have.string('json'); + expect(response.status).to.equal(200); + expect(response.body.message).to.equal('Credential successfully suspended.'); }); - it('returns update from status manager', async () => { - const updateStatus = sinon.fake.returns({ code: 200, message: 'Credential status successfully updated.' }); - const statusManagerStub = { updateStatus }; + it('returns update from unsuspended credential', async () => { + const unsuspendCredential = sinon.fake.returns({ + code: 200, + message: 'Credential successfully unsuspended.' + }); + const statusManagerStub = { unsuspendCredential }; await status.initializeStatusManager(statusManagerStub); const app = await build(); const response = await request(app) .post(updateEndpoint) - .send(getValidStatusUpdateBody()); + .send(getValidStatusUpdateBody(validCredentialId, 'unsuspended')); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(200); - expect(response.body.message).to.eql('Credential status successfully updated.'); + expect(response.status).to.equal(200); + expect(response.body.message).to.equal('Credential successfully unsuspended.'); }); - it('returns 404 for unknown cred id', async () => { - // const allocateStatus = sinon.fake.returns(getUnsignedVCWithStatus()) - const updateStatus = sinon.fake.rejects(missingCredIdErrorMessage); - const statusManagerStub = { updateStatus }; + it('returns 404 for unknown credential id', async () => { + const missingCredentialError = new Error(invalidCredentialIdErrorMessage); + missingCredentialError.code = 404; + const revokeCredential = sinon.fake.rejects(missingCredentialError); + const statusManagerStub = { revokeCredential }; await status.initializeStatusManager(statusManagerStub); const app = await build(); const response = await request(app) .post(updateEndpoint) - .send(getInvalidStatusUpdateBody()); + .send(getInvalidStatusUpdateBody(invalidCredentialId, 'revoked')); expect(response.header['content-type']).to.have.string('json'); - expect(response.status).to.eql(404); - console.log(response.body.message); - expect(response.body.message).to.contain('An error occurred in status-service-db: Credential ID not found.'); + expect(response.status).to.equal(404); + expect(response.body.message).to.contain('Unable to find credential with ID'); }); }); }); diff --git a/src/config.js b/src/config.js index 28d94e5..ba9cab8 100644 --- a/src/config.js +++ b/src/config.js @@ -9,9 +9,8 @@ export function setConfig() { } function getBooleanValue(value) { + value = value?.toLocaleLowerCase(); if ( - value === true || - value === 1 || value === 'true' || value === '1' || value === 'yes' || @@ -19,8 +18,6 @@ function getBooleanValue(value) { ) { return true; } else if ( - value === false || - value === 0 || value === 'false' || value === '0' || value === 'no' || @@ -31,22 +28,21 @@ function getBooleanValue(value) { return true; } -function getGeneralEnvs() { - const env = process.env; +function getGeneralEnvs(env) { return { port: env.PORT ? parseInt(env.PORT) : defaultPort, credStatusService: env.CRED_STATUS_SERVICE, credStatusDidSeed: env.CRED_STATUS_DID_SEED, - consoleLogLevel: env.CONSOLE_LOG_LEVEL?.toLocaleLowerCase() || defaultConsoleLogLevel, - logLevel: env.LOG_LEVEL?.toLocaleLowerCase() || defaultLogLevel, + consoleLogLevel: env.CONSOLE_LOG_LEVEL?.toLocaleLowerCase() ?? defaultConsoleLogLevel, + logLevel: env.LOG_LEVEL?.toLocaleLowerCase() ?? defaultLogLevel, enableAccessLogging: getBooleanValue(env.ENABLE_ACCESS_LOGGING), + enableHttpsForDev: getBooleanValue(env.ENABLE_HTTPS_FOR_DEV), errorLogFile: env.ERROR_LOG_FILE, allLogFile: env.ALL_LOG_FILE }; } -function getMongoDbEnvs() { - const env = process.env; +function getMongoDbEnvs(env) { return { statusCredSiteOrigin: env.STATUS_CRED_SITE_ORIGIN, credStatusDatabaseUrl: env.CRED_STATUS_DB_URL, @@ -64,16 +60,16 @@ function getMongoDbEnvs() { } function parseConfig() { - const env = process.env + const env = process.env; let serviceSpecificEnvs; switch (env.CRED_STATUS_SERVICE) { case 'mongodb': - serviceSpecificEnvs = getMongoDbEnvs(); + serviceSpecificEnvs = getMongoDbEnvs(env); break; default: - throw new Error('Encountered unsupported credential status service'); + throw new Error(`Encountered unsupported credential status service: ${env.CRED_STATUS_SERVICE}`); } - const generalEnvs = getGeneralEnvs(); + const generalEnvs = getGeneralEnvs(env); const config = Object.freeze({ ...generalEnvs, ...serviceSpecificEnvs diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index d5825ec..ada2edd 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -6,7 +6,7 @@ const errorHandler = (error, request, response, next) => { // for more detail const code = error.code | 500; - const message = `An error occurred in status-service-db: ${error.message || 'unknown error.'} See the logs for full details. If you are using docker compose, view the logs with 'docker compose logs', and just the status service logs with: 'docker compose logs status-service-db'`; + const message = `An error occurred in status-service-db: ${error.message ?? 'unknown error.'} See the logs for full details. If you are using docker compose, view the logs with 'docker compose logs', and just the status service logs with: 'docker compose logs status-service-db'`; const errorResponse = {code, message}; response.header('Content-Type', 'application/json'); return response.status(error.code).json(errorResponse); diff --git a/src/status.js b/src/status.js index 9b28c5b..fd3bdf6 100644 --- a/src/status.js +++ b/src/status.js @@ -63,7 +63,7 @@ async function initializeStatusManager(statusManager) { STATUS_LIST_MANAGER = await createMongoDbStatusManager(); break; default: - throw new Error('Encountered unsupported credential status service'); + throw new Error(`Encountered unsupported credential status service: ${credStatusService}`); } } @@ -86,20 +86,20 @@ async function updateStatus(credentialId, credentialStatus) { switch (credentialStatus) { case 'revoked': await statusManager.revokeCredential(credentialId); - return { code: 200, message: 'Credential status successfully revoked.' }; + return { code: 200, message: 'Credential successfully revoked.' }; case 'suspended': await statusManager.suspendCredential(credentialId); - return { code: 200, message: 'Credential status successfully suspended.' }; + return { code: 200, message: 'Credential successfully suspended.' }; case 'unsuspended': await statusManager.unsuspendCredential(credentialId); - return { code: 200, message: 'Credential status successfully unsuspended.' }; + return { code: 200, message: 'Credential successfully unsuspended.' }; default: return { code: 400, message: `Unsupported credential status: "${credentialStatus}"` }; } } catch (error) { return { - code: error.code || 500, - message: error.message || + code: error.code ?? 500, + message: error.message ?? `Unable to apply status "${credentialStatus}" to credential with ID "${credentialId}".` }; } diff --git a/src/test-fixtures/.env.testing b/src/test-fixtures/.env.testing index 2d72c57..1a06f8c 100644 --- a/src/test-fixtures/.env.testing +++ b/src/test-fixtures/.env.testing @@ -1,10 +1,19 @@ # PORT=4008 #default port is 4008 # the CRED_STATUS_* values are used to instantiate the status list manager -CRED_STATUS_REPO_OWNER=jchartrand -CRED_STATUS_REPO_NAME=status-test-three -CRED_STATUS_META_REPO_NAME=status-test-meta-three -CRED_STATUS_ACCESS_TOKEN= +CRED_STATUS_SERVICE=mongodb +STATUS_CRED_SITE_ORIGIN=https://credentials.example.edu +CRED_STATUS_DB_URL=mongodb+srv://user:pass@domain.mongodb.net?retryWrites=false +CRED_STATUS_DB_HOST=domain.mongodb.net # ignored if CRED_STATUS_DB_URL is configured +CRED_STATUS_DB_PORT=27017 # ignored if CRED_STATUS_DB_URL is configured +CRED_STATUS_DB_USER=testuser # ignored if CRED_STATUS_DB_URL is configured +CRED_STATUS_DB_PASS=testpass # ignored if CRED_STATUS_DB_URL is configured +CRED_STATUS_DB_NAME= +STATUS_CRED_TABLE_NAME= +USER_CRED_TABLE_NAME= +CONFIG_TABLE_NAME= +EVENT_TABLE_NAME= +CRED_EVENT_TABLE_NAME= CRED_STATUS_DID_SEED=z1AackbUm8U69ohKnihoRRFkXcXJd4Ra1PkAboQ2ZRy1ngB ERROR_LOG_FILE=logs/error.log diff --git a/src/test-fixtures/fixtures.js b/src/test-fixtures/fixtures.js index a68a6ae..d6cb80f 100644 --- a/src/test-fixtures/fixtures.js +++ b/src/test-fixtures/fixtures.js @@ -1,33 +1,51 @@ import testVC from './testVC.js'; - // "credentialStatus": - const credentialStatus = { +const validCredentialId = 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1'; +const invalidCredentialId = 'kj09ij'; +const invalidCredentialIdErrorMessage = `An error occurred in status-service-db: Unable to find credential with ID ${invalidCredentialId}`; + +const credentialStatus = { "id": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4#16", "type": "BitstringStatusListEntry", "statusPurpose": "revocation", "statusListIndex": 16, "statusListCredential": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4" -} +}; -const statusUpdateBody = { "credentialId": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", "credentialStatus": [{ "type": "BitstringStatusListCredential", "status": "revoked" }] } +const statusUpdateBody = { + "credentialId": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "credentialStatus": [{ "type": "BitstringStatusListCredential", "status": "revoked" }] +}; -const getUnsignedVC = () => JSON.parse(JSON.stringify(testVC)) +const getUnsignedVC = () => JSON.parse(JSON.stringify(testVC)); -const getValidStatusUpdateBody = () => JSON.parse(JSON.stringify(statusUpdateBody)) +const getValidStatusUpdateBody = (credentialId, status) => { + statusUpdateBody.credentialId = credentialId; + statusUpdateBody.credentialStatus[0].status = status; + return JSON.parse(JSON.stringify(statusUpdateBody)); +}; -const getInvalidStatusUpdateBody = () => { - const updateBody = getValidStatusUpdateBody() - updateBody.credentialId = 'kj09ij' - return updateBody -} +const getInvalidStatusUpdateBody = (credentialId, status) => { + const updateBody = getValidStatusUpdateBody(credentialId, status); + updateBody.credentialId = credentialId; + return updateBody; +}; -const getCredentialStatus = () => JSON.parse(JSON.stringify(credentialStatus)) +const getCredentialStatus = () => JSON.parse(JSON.stringify(credentialStatus)); const getUnsignedVCWithStatus = () => { const unsignedVCWithStatus = getUnsignedVC(); unsignedVCWithStatus.credentialStatus = getCredentialStatus(); - return unsignedVCWithStatus -} - - -export { getUnsignedVC, getCredentialStatus, getUnsignedVCWithStatus, getValidStatusUpdateBody, getInvalidStatusUpdateBody} + return unsignedVCWithStatus; +}; + +export { + validCredentialId, + invalidCredentialId, + invalidCredentialIdErrorMessage, + getUnsignedVC, + getCredentialStatus, + getUnsignedVCWithStatus, + getValidStatusUpdateBody, + getInvalidStatusUpdateBody +}; diff --git a/src/utils/logger.js b/src/utils/logger.js index 0c9880d..3ee63df 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -26,7 +26,7 @@ const level = () => { if (logLevel) { return logLevel; } else { - const env = process.env.NODE_ENV || 'development'; + const env = process.env.NODE_ENV ?? 'development'; const isDevelopment = env === 'development'; return isDevelopment ? 'silly' : 'warn'; }