Skip to content

Commit

Permalink
Feature/vr 153 (#94)
Browse files Browse the repository at this point in the history
* VR-153: abstracting PIN submission form to it's own page.

* VR-153: abstracting PIN submission form to it's own page. (un-staged files)

* VR-153: pin submission view unit tests

* VR-153: comments.

* VR-153: a linting, xss-scan and other clean-up procedures before making a PR.

* VR-153: leftovers, restoring action name.

* VR-153: version bump.

* VR-153: snapshot update.

* VR-154: a few copy changes.

* VR-153: copy changes.

* VR-153: missing snapshot.

* VR-153: saving before the meeting.

* VR-153: after testing locally from two nodes.

* VR-153: pushing latest.

* VR-153: after linting.

* VR-153: comments.

* VR-153: comments for Matt.

* Working pin submission

* Remove empty test file

* Version bump

* More validation

* Remove comments

---------

Co-authored-by: Matthew Dean <[email protected]>
  • Loading branch information
n3op2 and mattdean-digicatapult authored Jul 10, 2024
1 parent b8aff58 commit 2c42920
Show file tree
Hide file tree
Showing 47 changed files with 938 additions and 294 deletions.
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ services:
postgres-veritable-ui-bob:
image: postgres:16.3-alpine
container_name: postgres-veritable-ui-bob
ports:
- 5433:5432
volumes:
- postgres-veritable-ui-bob:/var/lib/postgresql/data
environment:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "veritable-ui",
"version": "0.7.6",
"version": "0.8.0",
"description": "UI for Veritable",
"main": "src/index.ts",
"type": "module",
Expand Down
5 changes: 0 additions & 5 deletions public/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -494,11 +494,6 @@ a.list-table.icon {
margin: 0;
}

.card-body {
max-width: 100%;
margin: 1rem;
}

.search-window {
margin-left: 10px;
}
Expand Down
14 changes: 13 additions & 1 deletion public/styles/new-invite.css
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,20 @@

#from-invite-invite-input-pin {
border-radius: 8px;
color: #abafb1;
background: var(--Input-defaultBackground, rgba(239, 241, 249, 0.6));

&:focus-visible {
outline: 2px solid var(--neutral-accent);
}

&:not(:placeholder-shown) {
&:invalid {
outline-color: var(--negative-accent);
}
&:valid {
outline-color: var(--positive-accent);
}
}
}

/* Mobile view */
Expand Down
13 changes: 13 additions & 0 deletions src/controllers/connection/__tests__/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { randomUUID } from 'crypto'
import type { ConnectionRow } from '../../../models/db/types.js'

export const notFoundCompanyNumber = '00000000'
export const invalidCompanyNumber = 'XXXXXXXX'
export const validCompanyNumber = '00000001'
Expand Down Expand Up @@ -50,6 +53,16 @@ export const validCompanyMap: Record<string, typeof validCompany> = {
[validCompanyNumberInactive]: validCompanyInactive,
}

export const validConnection: ConnectionRow = {
id: '4a5d4085-5924-43c6-b60d-754440332e3d',
agent_connection_id: randomUUID(),
created_at: new Date(),
updated_at: new Date(),
status: 'pending',
company_number: validCompanyNumber,
company_name: 'must be a valid company name',
}

const buildBase64Invite = (companyNumber: string) =>
Buffer.from(
JSON.stringify({
Expand Down
61 changes: 53 additions & 8 deletions src/controllers/connection/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { Readable } from 'node:stream'
import { pino } from 'pino'

import sinon from 'sinon'
import { Env } from '../../../env.js'
import type { ILogger } from '../../../logger.js'
import CompanyHouseEntity from '../../../models/companyHouseEntity.js'
import Database from '../../../models/db/index.js'
import { ConnectionRow } from '../../../models/db/types.js'
import EmailService from '../../../models/emailService/index.js'
import VeritableCloudagent from '../../../models/veritableCloudagent.js'
import ConnectionTemplates from '../../../views/connection.js'
import ConnectionTemplates from '../../../views/connection/connection.js'
import { FormFeedback } from '../../../views/newConnection/base.js'
import { FromInviteTemplates } from '../../../views/newConnection/fromInvite.js'
import { NewInviteTemplates } from '../../../views/newConnection/newInvite.js'
import { notFoundCompanyNumber, validCompanyMap, validCompanyNumber, validExistingCompanyNumber } from './fixtures.js'
import { PinSubmissionTemplates } from '../../../views/newConnection/pinSubmission.js'
import {
notFoundCompanyNumber,
validCompanyMap,
validCompanyNumber,
validConnection,
validExistingCompanyNumber,
} from './fixtures.js'

function templateFake(templateName: string, ...args: any[]) {
return Promise.resolve([templateName, args.join('-'), templateName].join('_'))
Expand All @@ -22,29 +30,58 @@ export const withConnectionMocks = () => {
const templateMock = {
listPage: (connections: ConnectionRow[]) =>
templateFake('list', connections[0].company_name, connections[0].status),
} as ConnectionTemplates
}
const mockLogger: ILogger = pino({ level: 'silent' })
const dbMock = {
get: () => Promise.resolve([{ company_name: 'foo', status: 'verified' }]),
} as unknown as Database
get: () =>
Promise.resolve([{ company_name: 'foo', status: 'unverified', agent_connection_id: 'AGENT_CONNECTION_ID' }]),
}
const cloudagentMock = {
proposeCredential: sinon.stub().resolves(),
}
const companyHouseMock = {
localCompanyHouseProfile: () =>
Promise.resolve({
company_number: 'COMPANY_NUMBER',
company_name: 'COMPANY_NAME',
}),
}
const pinSubmission = {
renderPinForm: (props: { connectionId: string; pin?: string; continuationFromInvite: boolean }) =>
templateFake('renderPinForm', props.connectionId, props.pin, props.continuationFromInvite),
renderSuccess: (props: { companyName: string; stepCount: number }) =>
templateFake('renderSuccess', props.companyName, props.stepCount),
}

return {
templateMock,
mockLogger,
dbMock,
cloudagentMock,
args: [
dbMock as unknown as Database,
cloudagentMock as unknown as VeritableCloudagent,
companyHouseMock as unknown as CompanyHouseEntity,
templateMock as ConnectionTemplates,
pinSubmission as unknown as PinSubmissionTemplates,
mockLogger,
] as const,
}
}

export const withNewConnectionMocks = () => {
const mockLogger: ILogger = pino({ level: 'silent' })
const mockTransactionDb = {
insert: () => Promise.resolve([{ id: '42' }]),
get: () => Promise.resolve([validConnection]),
update: () => Promise.resolve(),
}
const mockDb = {
get: (tableName: string, where?: Record<string, string>) => {
if (tableName !== 'connection') throw new Error('Invalid table')
if (where?.company_number === validCompanyNumber) return []
if (where?.company_number === validExistingCompanyNumber) return [{}]
if (where?.id === '4a5d4085-5924-43c6-b60d-754440332e3d') return [validConnection]
return []
},
withTransaction: (fn: Function) => {
Expand All @@ -67,6 +104,7 @@ export const withNewConnectionMocks = () => {
throw new Error('Invalid number')
},
} as unknown as CompanyHouseEntity

const mockCloudagent = {
createOutOfBandInvite: ({ companyName }: { companyName: string }) => {
return {
Expand Down Expand Up @@ -103,15 +141,20 @@ export const withNewConnectionMocks = () => {
} as unknown as NewInviteTemplates
const mockFromInvite = {
fromInviteFormPage: (feedback: FormFeedback) => templateFake('fromInvitePage', feedback.type),
fromInviteForm: ({ feedback, formStage }: any) =>
fromInviteForm: ({ feedback }: any) =>
templateFake(
'fromInviteForm',
feedback.type,
feedback.company?.company_name || '',
feedback.message || feedback.error || '',
formStage
feedback.message || feedback.error || ''
),
} as unknown as FromInviteTemplates
const mockPinForm = {
renderPinForm: (props: { connectionId: string; pin?: string; continuationFromInvite: boolean }) =>
templateFake('renderPinForm', props.connectionId, props.pin, props.continuationFromInvite),
renderSuccess: (props: { companyName: string; stepCount: number }) =>
templateFake('renderSuccess', props.companyName, props.stepCount),
} as unknown as PinSubmissionTemplates

const mockEnv = {
get: (name: string) => {
Expand All @@ -134,6 +177,7 @@ export const withNewConnectionMocks = () => {
mockEmail,
mockNewInvite,
mockFromInvite,
mockPinForm,
mockEnv,
mockLogger,
args: [
Expand All @@ -143,6 +187,7 @@ export const withNewConnectionMocks = () => {
mockEmail,
mockNewInvite,
mockFromInvite,
mockPinForm,
mockEnv,
mockLogger,
] as const,
Expand Down
88 changes: 79 additions & 9 deletions src/controllers/connection/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import sinon from 'sinon'
import { toHTMLString, withConnectionMocks } from './helpers.js'

import { ConnectionController } from '../index.js'
import { validConnection } from './fixtures.js'

describe('ConnectionController', () => {
afterEach(() => {
Expand All @@ -13,27 +14,96 @@ describe('ConnectionController', () => {

describe('listConnections', () => {
it('should return rendered list template', async () => {
let { dbMock, mockLogger, templateMock } = withConnectionMocks()
const controller = new ConnectionController(dbMock, templateMock, mockLogger)
let { args } = withConnectionMocks()
const controller = new ConnectionController(...args)
const result = await controller.listConnections().then(toHTMLString)
expect(result).to.equal('list_foo-verified_list')
expect(result).to.equal('list_foo-unverified_list')
})

it('should call db as expected', async () => {
let { dbMock, mockLogger, templateMock } = withConnectionMocks()
const controller = new ConnectionController(dbMock, templateMock, mockLogger)
const spy = sinon.spy(dbMock, 'get')
let { dbMock, args } = withConnectionMocks()
const controller = new ConnectionController(...args)
const spy: sinon.SinonSpy = sinon.spy(dbMock, 'get')
await controller.listConnections().then(toHTMLString)
expect(spy.calledWith('connection', {}, [['updated_at', 'desc']])).to.equal(true)
})
it('should call db as expected', async () => {
let { dbMock, mockLogger, templateMock } = withConnectionMocks()
const controller = new ConnectionController(dbMock, templateMock, mockLogger)
const spy = sinon.spy(dbMock, 'get')
let { dbMock, args } = withConnectionMocks()
const controller = new ConnectionController(...args)
const spy: sinon.SinonSpy = sinon.spy(dbMock, 'get')
await controller.listConnections('dig').then(toHTMLString)
const search = 'dig'
const query = search ? [['company_name', 'ILIKE', `%${search}%`]] : {}
expect(spy.calledWith('connection', query, [['updated_at', 'desc']])).to.equal(true)
})
})

describe('renderPinCode', () => {
it('should render form', async () => {
let { args } = withConnectionMocks()
const controller = new ConnectionController(...args)
const result = await controller.renderPinCode(validConnection.id, '123456').then(toHTMLString)
expect(result).to.equal('renderPinForm_4a5d4085-5924-43c6-b60d-754440332e3d-123456-false_renderPinForm')
})
})

describe('submitPinCode', () => {
it('should render error if it is longer than 6 digits', async () => {
let { args } = withConnectionMocks()
const controller = new ConnectionController(...args)
const result = await controller
.submitPinCode({ action: 'submitPinCode', pin: '123456782' }, validConnection.id)
.then(toHTMLString)
expect(result).to.equal('renderPinForm_4a5d4085-5924-43c6-b60d-754440332e3d-123456782-false_renderPinForm')
})

it('also should render error if it combined characters and numbers', async () => {
let { args } = withConnectionMocks()
const controller = new ConnectionController(...args)
const result = await controller
.submitPinCode({ action: 'submitPinCode', pin: '1235235asdasd' }, validConnection.id)
.then(toHTMLString)
expect(result).to.equal('renderPinForm_4a5d4085-5924-43c6-b60d-754440332e3d-1235235asdasd-false_renderPinForm')
})

it('should accept only numbers', async () => {
let { args } = withConnectionMocks()
const controller = new ConnectionController(...args)
const result = await controller
.submitPinCode({ action: 'submitPinCode', pin: 'not-valid-code' }, validConnection.id)
.then(toHTMLString)
expect(result).to.equal('renderPinForm_4a5d4085-5924-43c6-b60d-754440332e3d-not-valid-code-false_renderPinForm')
})

it('renders a success screen', async () => {
let { args, cloudagentMock } = withConnectionMocks()
const controller = new ConnectionController(...args)
const result = await controller
.submitPinCode({ action: 'submitPinCode', pin: '111111' }, validConnection.id)
.then(toHTMLString)
expect(result).to.equal('renderSuccess_foo-2_renderSuccess')
expect(cloudagentMock.proposeCredential.calledOnce).to.equal(true)
expect(cloudagentMock.proposeCredential.firstCall.args).to.deep.equal([
'AGENT_CONNECTION_ID',
{
schemaName: 'COMPANY_DETAILS',
schemaVersion: '1.0.0',
attributes: [
{
name: 'company_name',
value: 'COMPANY_NAME',
},
{
name: 'company_number',
value: 'COMPANY_NUMBER',
},
{
name: 'pin',
value: '111111',
},
],
},
])
})
})
})
Loading

0 comments on commit 2c42920

Please sign in to comment.