Skip to content

Commit b3d208b

Browse files
committed
ads specs for routes and services and README
1 parent a5d71f5 commit b3d208b

9 files changed

Lines changed: 898 additions & 8 deletions

File tree

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Identity Doc Auth Mock
2+
3+
This project is a mock server for identity document authentication and verification workflows. It simulates endpoints for document verification, MRZ checks, and related flows, and is designed for integration testing and development without requiring real third-party services.
4+
5+
## Features
6+
7+
- **Express.js API** with endpoints for document verification, MRZ, and health checks
8+
- **Redis integration** for storing and retrieving transaction tokens
9+
- **Mocked webhook callbacks** for simulating document upload events
10+
- **Header and payload validation** for all endpoints
11+
- **Configurable via environment variables**
12+
13+
## Endpoints
14+
15+
### `/api/3.0/EmailAuthScore`
16+
- **POST**: Document verification (requires `authorization` and `content-type: application/json` headers)
17+
18+
### `/docv/doc_request`
19+
- **POST**: Initiate a document verification request
20+
21+
### `/dos/mrz`
22+
- **POST**: MRZ check (requires `client-id`, `x-correlation-id`, `client-secret`, and `content-type: application/json` headers)
23+
24+
### `/dos/healthcheck`
25+
- **GET**: Health check endpoint
26+
27+
## Setup
28+
29+
1. **Install dependencies**
30+
```sh
31+
yarn install
32+
# or
33+
npm install
34+
```
35+
36+
2. **Configure environment variables**
37+
Create a `.env` file in the project root:
38+
```
39+
PORT=3002
40+
API_KEY=your-api-key
41+
DEFAULT_IDP_DOCV_CALLBACK_URL=https://example.com/callback
42+
IDP_WEBHOOK_PATH=/webhook
43+
WEBHOOK_SECRET=your-webhook-secret
44+
REDIS_URL=redis://localhost:6379
45+
```
46+
47+
3. **Start the server**
48+
```sh
49+
yarn start
50+
# or
51+
npm start
52+
```
53+
54+
## Testing
55+
56+
- **Run all tests**
57+
```sh
58+
yarn test
59+
# or
60+
npm test
61+
```
62+
63+
- **Lint the code**
64+
```sh
65+
yarn lint
66+
# or
67+
npm run lint
68+
```
69+
70+
## Development
71+
72+
- Endpoints and logic are organized under the `routes/` and `services/` directories.
73+
- Mock data for document verification is in `data/docv/results.js`.
74+
- Tests are in the `test/` directory and use Mocha, Chai, and Sinon.
75+
76+
## Public domain
77+
78+
This project is in the public domain within the United States, and
79+
copyright and related rights in the work worldwide are waived through
80+
the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/).
81+
82+
All contributions to this project will be released under the CC0
83+
dedication. By submitting a pull request, you are agreeing to comply
84+
with this waiver of copyright interest.
85+
86+
---
87+
**Note:** This project is for development and testing purposes only. Do not use in production environments.

eslint.config.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import js from "@eslint/js";
2+
import globals from "globals";
3+
import { defineConfig } from "eslint/config";
4+
5+
export default defineConfig([
6+
{ files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
7+
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
8+
]);

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scripts": {
77
"start": "node server.js",
88
"dev": "nodemon server.js",
9-
"test": "NODE_ENV=test mocha"
9+
"test": "NODE_ENV=test mocha **/*.test.js --exit"
1010
},
1111
"dependencies": {
1212
"axios": "^1.9.0",
@@ -16,10 +16,14 @@
1616
"redis": "^5.5.6"
1717
},
1818
"devDependencies": {
19+
"@eslint/js": "^9.32.0",
1920
"chai": "^5.2.1",
21+
"eslint": "^9.32.0",
22+
"globals": "^16.3.0",
2023
"mocha": "^11.7.1",
2124
"nodemon": "^3.0.0",
2225
"proxyquire": "^2.1.3",
26+
"sinon": "^21.0.0",
2327
"supertest": "^7.1.4"
2428
},
2529
"private": true,

routes/id_plus.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ router.post('/', (req, res) => {
1414

1515
const { modules } = req.body;
1616

17-
if (!Array.isArray(modules) &&
18-
modules.length !== 1 &&
19-
modules[0] !== "documentverification"
17+
if (!Array.isArray(modules) ||
18+
modules.length !== 1 ||
19+
modules[0] !== "documentVerification"
2020
) {
2121
return res.status(400).json({ error: 'Invalid modules' });
2222
}

test/routes/docv.test.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const express = require('express');
2+
const request = require('supertest');
3+
const sinon = require('sinon');
4+
const proxyquire = require('proxyquire').noCallThru();
5+
const { expect } = require('chai');
6+
7+
describe('POST /docv/doc_request', function () {
8+
let app, redisStub, axiosStub, authorizedStub;
9+
10+
beforeEach(async function () {
11+
// Stub Redis client
12+
redisStub = {
13+
connect: sinon.stub().resolves(),
14+
setEx: sinon.stub().resolves()
15+
};
16+
17+
// Stub axios
18+
axiosStub = { post: sinon.stub().resolves() };
19+
20+
// Stub authorized
21+
authorizedStub = sinon.stub();
22+
23+
// Proxyquire the route with stubs
24+
const docvRoute = proxyquire('../../routes/docv', {
25+
axios: axiosStub,
26+
crypto: require('crypto'),
27+
redis: { createClient: () => redisStub },
28+
'../services/id_plus': { authorized: authorizedStub }
29+
});
30+
31+
// Set up express app
32+
app = express();
33+
app.use(express.json());
34+
app.use('/docv', docvRoute);
35+
36+
// Set required env vars
37+
process.env.DEFAULT_IDP_DOCV_CALLBACK_URL = 'https://example.com/callback';
38+
process.env.IDP_WEBHOOK_PATH = '/webhook';
39+
process.env.WEBHOOK_SECRET = 'secret';
40+
});
41+
42+
it('should reject missing or invalid headers', async function () {
43+
const res = await request(app)
44+
.post('/docv/doc_request')
45+
.send({});
46+
expect(res.status).to.equal(400);
47+
expect(res.body).to.have.property('error', 'Missing or invalid headers');
48+
});
49+
50+
it('should reject unauthorized requests', async function () {
51+
authorizedStub.returns(false);
52+
const res = await request(app)
53+
.post('/docv/doc_request')
54+
.set('authorization', 'Bearer token')
55+
.set('content-type', 'application/json')
56+
.send({});
57+
expect(res.status).to.equal(401);
58+
expect(res.body).to.have.property('error', 'Unauthorized');
59+
});
60+
61+
it('should reject invalid documentType', async function () {
62+
authorizedStub.returns(true);
63+
const res = await request(app)
64+
.post('/docv/doc_request')
65+
.set('authorization', 'Bearer token')
66+
.set('content-type', 'application/json')
67+
.send({ config: { documentType: 'foo', redirect: {} } });
68+
expect(res.status).to.equal(400);
69+
expect(res.body).to.have.property('error', 'Invalid or missing documentType');
70+
});
71+
72+
it('should reject invalid redirect method', async function () {
73+
authorizedStub.returns(true);
74+
const res = await request(app)
75+
.post('/docv/doc_request')
76+
.set('authorization', 'Bearer token')
77+
.set('content-type', 'application/json')
78+
.send({ config: { documentType: 'license', redirect: { method: 'PUT', url: 'https://example.com/callback' } } });
79+
expect(res.status).to.equal(400);
80+
expect(res.body).to.have.property('error', 'Invalid or missing redirect method');
81+
});
82+
83+
it('should reject invalid redirect url', async function () {
84+
authorizedStub.returns(true);
85+
const res = await request(app)
86+
.post('/docv/doc_request')
87+
.set('authorization', 'Bearer token')
88+
.set('content-type', 'application/json')
89+
.send({ config: { documentType: 'license', redirect: { method: 'GET', url: 'not-a-url' } } });
90+
expect(res.status).to.equal(400);
91+
expect(res.body).to.have.property('error', 'Invalid redirect url');
92+
});
93+
94+
it('should accept valid request and store in redis', async function () {
95+
authorizedStub.returns(true);
96+
const res = await request(app)
97+
.post('/docv/doc_request')
98+
.set('authorization', 'Bearer token')
99+
.set('content-type', 'application/json')
100+
.send({ config: { documentType: 'passport', redirect: { method: 'POST', url: 'https://example.com/callback' } } });
101+
expect(res.status).to.equal(200);
102+
expect(res.body.data).to.have.property('url', 'https://example.com/callback');
103+
expect(res.body.data).to.have.property('docvTransactionToken');
104+
expect(redisStub.setEx.calledOnce).to.be.true;
105+
expect(axiosStub.post.calledOnce).to.be.true;
106+
});
107+
});

test/routes/dos.test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
const express = require('express');
2+
const request = require('supertest');
3+
const { expect } = require('chai');
4+
5+
const dosRoute = require('../../routes/dos');
6+
7+
describe('DOS Route', function () {
8+
let app;
9+
10+
before(function () {
11+
app = express();
12+
app.use(express.json());
13+
app.use('/dos', dosRoute);
14+
});
15+
16+
describe('GET /dos/healthcheck', function () {
17+
it('should return status UP', async function () {
18+
const res = await request(app).get('/dos/healthcheck');
19+
expect(res.status).to.equal(200);
20+
expect(res.body).to.deep.equal({ status: 'UP' });
21+
});
22+
});
23+
24+
describe('POST /dos/mrz', function () {
25+
const validHeaders = {
26+
'client-id': 'test-client',
27+
'x-correlation-id': 'corr-id',
28+
'client-secret': 'secret',
29+
'content-type': 'application/json'
30+
};
31+
32+
it('should return 400 for missing headers', async function () {
33+
const res = await request(app)
34+
.post('/dos/mrz')
35+
.send({});
36+
expect(res.status).to.equal(400);
37+
expect(res.body).to.have.property('error', 'Missing or invalid headers');
38+
});
39+
40+
it('should return 200 and response YES for valid headers', async function () {
41+
const res = await request(app)
42+
.post('/dos/mrz')
43+
.set(validHeaders)
44+
.send({});
45+
expect(res.status).to.equal(200);
46+
expect(res.body).to.deep.equal({ response: 'YES' });
47+
});
48+
49+
it('should return 400 if content-type is not application/json', async function () {
50+
const headers = { ...validHeaders, 'content-type': 'text/plain' };
51+
const res = await request(app)
52+
.post('/dos/mrz')
53+
.set(headers)
54+
.send('');
55+
expect(res.status).to.equal(400);
56+
expect(res.body).to.have.property('error', 'Missing or invalid headers');
57+
});
58+
59+
it('should return 400 if any required header is missing', async function () {
60+
const headers = { ...validHeaders };
61+
delete headers['client-secret'];
62+
const res = await request(app)
63+
.post('/dos/mrz')
64+
.set(headers)
65+
.send({});
66+
expect(res.status).to.equal(400);
67+
expect(res.body).to.have.property('error', 'Missing or invalid headers');
68+
});
69+
});
70+
});

test/routes/id_plus.test.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const express = require('express');
2+
const request = require('supertest');
3+
const sinon = require('sinon');
4+
const { expect } = require('chai');
5+
const proxyquire = require('proxyquire').noCallThru();
6+
7+
describe('id_plus Route', function () {
8+
let app, documentVerificationStub;
9+
10+
beforeEach(function () {
11+
documentVerificationStub = sinon.stub().callsFake((req, res) => {
12+
res.status(200).json({ ok: true });
13+
});
14+
15+
const idPlusRoute = proxyquire('../../routes/id_plus', {
16+
'../services/id_plus': {
17+
authorized: () => true,
18+
documentVerification: documentVerificationStub
19+
}
20+
});
21+
22+
app = express();
23+
app.use(express.json());
24+
app.use('/api/3.0/EmailAuthScore', idPlusRoute);
25+
});
26+
27+
it('should return 400 for missing headers', async function () {
28+
const res = await request(app)
29+
.post('/api/3.0/EmailAuthScore')
30+
.send({});
31+
expect(res.status).to.equal(400);
32+
expect(res.body).to.have.property('error', 'Missing or invalid headers');
33+
});
34+
35+
it('should return 400 for invalid content-type', async function () {
36+
const res = await request(app)
37+
.post('/api/3.0/EmailAuthScore')
38+
.set('authorization', 'Bearer token')
39+
.set('content-type', 'text/plain')
40+
.send('');
41+
expect(res.status).to.equal(400);
42+
expect(res.body).to.have.property('error', 'Missing or invalid headers');
43+
});
44+
45+
it('should return 400 for invalid modules', async function () {
46+
const res = await request(app)
47+
.post('/api/3.0/EmailAuthScore')
48+
.set('authorization', 'Bearer token')
49+
.set('content-type', 'application/json')
50+
.send({ modules: [] });
51+
expect(res.status).to.equal(400);
52+
expect(res.body).to.have.property('error', 'Invalid modules');
53+
});
54+
55+
it('should call documentVerification for valid request', async function () {
56+
const res = await request(app)
57+
.post('/api/3.0/EmailAuthScore')
58+
.set('authorization', 'Bearer token')
59+
.set('content-type', 'application/json')
60+
.send({ modules: ['documentVerification'] });
61+
expect(res.status).to.equal(200);
62+
expect(res.body).to.deep.equal({ ok: true });
63+
expect(documentVerificationStub.calledOnce).to.be.true;
64+
});
65+
});

0 commit comments

Comments
 (0)