Skip to content

Commit

Permalink
Chore/connection controller (#10)
Browse files Browse the repository at this point in the history
* Connection list route and placeholder view

* Rename controller

* Coontroller tests
  • Loading branch information
mattdean-digicatapult authored May 8, 2024
1 parent deef224 commit a53af57
Show file tree
Hide file tree
Showing 14 changed files with 247 additions and 17 deletions.
19 changes: 19 additions & 0 deletions migrations/20240508094320_add_update_index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = async function (knex) {
await knex.schema.alterTable('connection', (def) => {
def.index('updated_at', 'idx_connection_updated_at')
})
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function (knex) {
await knex.schema.alterTable('connection', (def) => {
def.dropIndex('updated_at', 'idx_connection_updated_at')
})
}
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.1.5",
"version": "0.1.6",
"description": "UI for Veritable",
"main": "src/index.ts",
"type": "commonjs",
Expand Down
7 changes: 5 additions & 2 deletions src/controllers/__tests__/example.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { counterMock, mockLogger, templateMock, toHTMLString } from './helpers'

import { RootController } from '../example'

describe('ExampleController', async () => {
let { expect } = await import('chai')
describe('ExampleController', () => {
let expect: Chai.ExpectStatic
before(async () => {
expect = (await import('chai')).expect
})

afterEach(() => {
sinon.restore()
Expand Down
30 changes: 30 additions & 0 deletions src/controllers/connection/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Readable } from 'node:stream'

import pino from 'pino'

import Database from '../../../models/db/index.js'
import ConnectionTemplates from '../../../views/connection'

export const withMocks = () => {
const templateMock = {
listPage: (connections) => `list_${connections.map((c) => `${c.company_name}-${c.status}`).join('_')}_list`,
} as ConnectionTemplates
const mockLogger = pino({ level: 'silent' })
const dbMock = {
get: () => Promise.resolve([{ company_name: 'foo', status: 'verified' }]),
} as unknown as Database

return {
templateMock,
mockLogger,
dbMock,
}
}

export const toHTMLString = async (stream: Readable) => {
const chunks: Uint8Array[] = []
for await (const chunk of stream) {
chunks.push(chunk as Uint8Array)
}
return Buffer.concat(chunks).toString('utf8')
}
34 changes: 34 additions & 0 deletions src/controllers/connection/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, it } from 'mocha'
import sinon from 'sinon'

import { toHTMLString, withMocks } from './helpers'

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

describe('ConnectionController', () => {
let expect: Chai.ExpectStatic
before(async () => {
expect = (await import('chai')).expect
})

afterEach(() => {
sinon.restore()
})

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

it('should call db as expected', async () => {
let { dbMock, mockLogger, templateMock } = withMocks()
const controller = new ConnectionController(dbMock, templateMock, mockLogger)
const spy = sinon.spy(dbMock, 'get')
await controller.listConnections().then(toHTMLString)
expect(spy.calledWith('connection', {}, [['updated_at', 'desc']])).to.equal(true)
})
})
})
33 changes: 33 additions & 0 deletions src/controllers/connection/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Get, Produces, Route, SuccessResponse } from 'tsoa'
import { inject, injectable, singleton } from 'tsyringe'

import { Logger, type ILogger } from '../../logger.js'
import Database from '../../models/db/index.js'
import ConnectionTemplates from '../../views/connection.js'
import { HTML, HTMLController } from '../HTMLController.js'

@singleton()
@injectable()
@Route('/connection')
@Produces('text/html')
export class ConnectionController extends HTMLController {
constructor(
private db: Database,
private connectionTemplates: ConnectionTemplates,
@inject(Logger) private logger: ILogger
) {
super()
this.logger = logger.child({ controller: '/' })
}

/**
* Retrieves the connection list page
*/
@SuccessResponse(200)
@Get('/')
public async listConnections(): Promise<HTML> {
this.logger.debug('connections page requested')
const connections = await this.db.get('connection', {}, [['updated_at', 'desc']])
return this.html(this.connectionTemplates.listPage(connections))
}
}
7 changes: 5 additions & 2 deletions src/models/__tests__/counter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { describe, it } from 'mocha'

import Counter from '../counter'

describe('Counter', async () => {
let { expect } = await import('chai')
describe('Counter', () => {
let expect: Chai.ExpectStatic
before(async () => {
expect = (await import('chai')).expect
})

describe('get', () => {
it('should return counter state 0 initially', async () => {
Expand Down
71 changes: 71 additions & 0 deletions src/views/__tests__/connection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it } from 'mocha'

import ConnectionTemplates from '../connection'

describe('ConnectionTemplates', () => {
let expect: Chai.ExpectStatic
before(async () => {
expect = (await import('chai')).expect
})

describe('listPage', () => {
it('should render with no connections', async () => {
const templates = new ConnectionTemplates()
const rendered = await templates.listPage([])
expect(rendered).to.matchSnapshot()
})

it('should render with single connection', async () => {
const templates = new ConnectionTemplates()
const rendered = await templates.listPage([
{
company_name: 'Company A',
status: 'disconnected',
},
])
expect(rendered).to.matchSnapshot()
})

it('should escape html in name', async () => {
const templates = new ConnectionTemplates()
const rendered = await templates.listPage([
{
company_name: '<div>I own you</div>',
status: 'verified_both',
},
])
expect(rendered).to.matchSnapshot()
})

it('should render multiple with each status', async () => {
const templates = new ConnectionTemplates()
const rendered = await templates.listPage([
{
company_name: 'Company A',
status: 'disconnected',
},
{
company_name: 'Company B',
status: 'pending',
},
{
company_name: 'Company C',
status: 'unverified',
},
{
company_name: 'Company D',
status: 'verified_both',
},
{
company_name: 'Company E',
status: 'verified_them',
},
{
company_name: 'Company F',
status: 'verified_us',
},
])
expect(rendered).to.matchSnapshot()
})
})
})
9 changes: 9 additions & 0 deletions src/views/__tests__/connection.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ConnectionTemplates listPage should escape html in name 1`] = `"<!DOCTYPE html><html lang=\\"en\\"><head><script src=\\"lib/htmx.org/htmx.min.js\\"></script><script src=\\"lib/htmx.org/ext/json-enc.js\\"></script><link rel=\\"icon\\" type=\\"image/ico\\" sizes=\\"48x48\\" href=\\"/public/images/favicon.ico\\"/><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/public/styles/main.css\\"/><title>Veritable - Connections</title></head><body hx-ext=\\"json-enc\\"><div><div>&lt;div>I own you&lt;/div></div><div>verified_both</div></div></body></html>"`;
exports[`ConnectionTemplates listPage should render multiple with each status 1`] = `"<!DOCTYPE html><html lang=\\"en\\"><head><script src=\\"lib/htmx.org/htmx.min.js\\"></script><script src=\\"lib/htmx.org/ext/json-enc.js\\"></script><link rel=\\"icon\\" type=\\"image/ico\\" sizes=\\"48x48\\" href=\\"/public/images/favicon.ico\\"/><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/public/styles/main.css\\"/><title>Veritable - Connections</title></head><body hx-ext=\\"json-enc\\"><div><div>Company A</div><div>disconnected</div></div><div><div>Company B</div><div>pending</div></div><div><div>Company C</div><div>unverified</div></div><div><div>Company D</div><div>verified_both</div></div><div><div>Company E</div><div>verified_them</div></div><div><div>Company F</div><div>verified_us</div></div></body></html>"`;
exports[`ConnectionTemplates listPage should render with no connections 1`] = `"<!DOCTYPE html><html lang=\\"en\\"><head><script src=\\"lib/htmx.org/htmx.min.js\\"></script><script src=\\"lib/htmx.org/ext/json-enc.js\\"></script><link rel=\\"icon\\" type=\\"image/ico\\" sizes=\\"48x48\\" href=\\"/public/images/favicon.ico\\"/><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/public/styles/main.css\\"/><title>Veritable - Connections</title></head><body hx-ext=\\"json-enc\\"></body></html>"`;
exports[`ConnectionTemplates listPage should render with single connection 1`] = `"<!DOCTYPE html><html lang=\\"en\\"><head><script src=\\"lib/htmx.org/htmx.min.js\\"></script><script src=\\"lib/htmx.org/ext/json-enc.js\\"></script><link rel=\\"icon\\" type=\\"image/ico\\" sizes=\\"48x48\\" href=\\"/public/images/favicon.ico\\"/><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/public/styles/main.css\\"/><title>Veritable - Connections</title></head><body hx-ext=\\"json-enc\\"><div><div>Company A</div><div>disconnected</div></div></body></html>"`;
5 changes: 4 additions & 1 deletion src/views/__tests__/example.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { describe, it } from 'mocha'
import ExampleTemplates from '../example'

describe('RootTemplates', async () => {
let { expect } = await import('chai')
let expect: Chai.ExpectStatic
before(async () => {
expect = (await import('chai')).expect
})

describe('Root', () => {
it('should render root page', async () => {
Expand Down
14 changes: 7 additions & 7 deletions src/views/__tests__/example.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Button should render the disabled button 1`] = `"<div class=\\"button-group\\"><button disabled hx-target=\\"closest .button-group\\" hx-trigger=\\"counter-loaded from:body\\" hx-get=\\"/example/button\\" hx-swap=\\"outerHTML\\">Click me!</button></div>"`;
exports[`RootTemplates Button should render the disabled button 1`] = `"<div class=\\"button-group\\"><button disabled hx-target=\\"closest .button-group\\" hx-trigger=\\"counter-loaded from:body\\" hx-get=\\"/example/button\\" hx-swap=\\"outerHTML\\">Click me!</button></div>"`;
exports[`Button should render the enabled button 1`] = `"<div class=\\"button-group\\"><button hx-target=\\"closest .button-group\\" hx-post=\\"/example/button\\" hx-swap=\\"outerHTML\\">Click me!</button></div>"`;
exports[`RootTemplates Button should render the enabled button 1`] = `"<div class=\\"button-group\\"><button hx-target=\\"closest .button-group\\" hx-post=\\"/example/button\\" hx-swap=\\"outerHTML\\">Click me!</button></div>"`;
exports[`Counter should render the counter on second call with value 1 1`] = `"<div id=\\"counter\\" hx-get=\\"/example/counter\\" hx-trigger=\\"button-click from:body\\" hx-swap=\\"outerHTML\\"><span>1</span><img class=\\"spinner htmx-indicator\\" src=\\"/public/images/spinner.svg\\"/></div>"`;
exports[`RootTemplates Counter should render the counter on second call with value 1 1`] = `"<div id=\\"counter\\" hx-get=\\"/example/counter\\" hx-trigger=\\"button-click from:body\\" hx-swap=\\"outerHTML\\"><span>1</span><img class=\\"spinner htmx-indicator\\" src=\\"/public/images/spinner.svg\\"/></div>"`;
exports[`Counter should render the counter with value 0 1`] = `"<div id=\\"counter\\" hx-get=\\"/example/counter\\" hx-trigger=\\"button-click from:body\\" hx-swap=\\"outerHTML\\"><span>0</span><img class=\\"spinner htmx-indicator\\" src=\\"/public/images/spinner.svg\\"/></div>"`;
exports[`RootTemplates Counter should render the counter with value 0 1`] = `"<div id=\\"counter\\" hx-get=\\"/example/counter\\" hx-trigger=\\"button-click from:body\\" hx-swap=\\"outerHTML\\"><span>0</span><img class=\\"spinner htmx-indicator\\" src=\\"/public/images/spinner.svg\\"/></div>"`;
exports[`Root should escape html in title 1`] = `"<!DOCTYPE html><html lang=\\"en\\"><head><script src=\\"lib/htmx.org/htmx.min.js\\"></script><script src=\\"lib/htmx.org/ext/json-enc.js\\"></script><link rel=\\"icon\\" type=\\"image/ico\\" sizes=\\"48x48\\" href=\\"/public/images/favicon.ico\\"/><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/public/styles/main.css\\"/><title>&lt;div>Malicious Content&lt;/div></title></head><body hx-ext=\\"json-enc\\"><div id=\\"counter\\" hx-get=\\"/example/counter\\" hx-trigger=\\"button-click from:body\\" hx-swap=\\"outerHTML\\"><span>0</span><img class=\\"spinner htmx-indicator\\" src=\\"/public/images/spinner.svg\\"/></div><div class=\\"button-group\\"><button hx-target=\\"closest .button-group\\" hx-post=\\"/example/button\\" hx-swap=\\"outerHTML\\">Click me!</button></div></body></html>"`;
exports[`RootTemplates Root should escape html in title 1`] = `"<!DOCTYPE html><html lang=\\"en\\"><head><script src=\\"lib/htmx.org/htmx.min.js\\"></script><script src=\\"lib/htmx.org/ext/json-enc.js\\"></script><link rel=\\"icon\\" type=\\"image/ico\\" sizes=\\"48x48\\" href=\\"/public/images/favicon.ico\\"/><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/public/styles/main.css\\"/><title>&lt;div>Malicious Content&lt;/div></title></head><body hx-ext=\\"json-enc\\"><div id=\\"counter\\" hx-get=\\"/example/counter\\" hx-trigger=\\"button-click from:body\\" hx-swap=\\"outerHTML\\"><span>0</span><img class=\\"spinner htmx-indicator\\" src=\\"/public/images/spinner.svg\\"/></div><div class=\\"button-group\\"><button hx-target=\\"closest .button-group\\" hx-post=\\"/example/button\\" hx-swap=\\"outerHTML\\">Click me!</button></div></body></html>"`;
exports[`Root should render counter 1 on second load 1`] = `"<!DOCTYPE html><html lang=\\"en\\"><head><script src=\\"lib/htmx.org/htmx.min.js\\"></script><script src=\\"lib/htmx.org/ext/json-enc.js\\"></script><link rel=\\"icon\\" type=\\"image/ico\\" sizes=\\"48x48\\" href=\\"/public/images/favicon.ico\\"/><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/public/styles/main.css\\"/><title>title</title></head><body hx-ext=\\"json-enc\\"><div id=\\"counter\\" hx-get=\\"/example/counter\\" hx-trigger=\\"button-click from:body\\" hx-swap=\\"outerHTML\\"><span>1</span><img class=\\"spinner htmx-indicator\\" src=\\"/public/images/spinner.svg\\"/></div><div class=\\"button-group\\"><button hx-target=\\"closest .button-group\\" hx-post=\\"/example/button\\" hx-swap=\\"outerHTML\\">Click me!</button></div></body></html>"`;
exports[`RootTemplates Root should render counter 1 on second load 1`] = `"<!DOCTYPE html><html lang=\\"en\\"><head><script src=\\"lib/htmx.org/htmx.min.js\\"></script><script src=\\"lib/htmx.org/ext/json-enc.js\\"></script><link rel=\\"icon\\" type=\\"image/ico\\" sizes=\\"48x48\\" href=\\"/public/images/favicon.ico\\"/><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/public/styles/main.css\\"/><title>title</title></head><body hx-ext=\\"json-enc\\"><div id=\\"counter\\" hx-get=\\"/example/counter\\" hx-trigger=\\"button-click from:body\\" hx-swap=\\"outerHTML\\"><span>1</span><img class=\\"spinner htmx-indicator\\" src=\\"/public/images/spinner.svg\\"/></div><div class=\\"button-group\\"><button hx-target=\\"closest .button-group\\" hx-post=\\"/example/button\\" hx-swap=\\"outerHTML\\">Click me!</button></div></body></html>"`;
exports[`Root should render root page 1`] = `"<!DOCTYPE html><html lang=\\"en\\"><head><script src=\\"lib/htmx.org/htmx.min.js\\"></script><script src=\\"lib/htmx.org/ext/json-enc.js\\"></script><link rel=\\"icon\\" type=\\"image/ico\\" sizes=\\"48x48\\" href=\\"/public/images/favicon.ico\\"/><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/public/styles/main.css\\"/><title>title</title></head><body hx-ext=\\"json-enc\\"><div id=\\"counter\\" hx-get=\\"/example/counter\\" hx-trigger=\\"button-click from:body\\" hx-swap=\\"outerHTML\\"><span>0</span><img class=\\"spinner htmx-indicator\\" src=\\"/public/images/spinner.svg\\"/></div><div class=\\"button-group\\"><button hx-target=\\"closest .button-group\\" hx-post=\\"/example/button\\" hx-swap=\\"outerHTML\\">Click me!</button></div></body></html>"`;
exports[`RootTemplates Root should render root page 1`] = `"<!DOCTYPE html><html lang=\\"en\\"><head><script src=\\"lib/htmx.org/htmx.min.js\\"></script><script src=\\"lib/htmx.org/ext/json-enc.js\\"></script><link rel=\\"icon\\" type=\\"image/ico\\" sizes=\\"48x48\\" href=\\"/public/images/favicon.ico\\"/><link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/public/styles/main.css\\"/><title>title</title></head><body hx-ext=\\"json-enc\\"><div id=\\"counter\\" hx-get=\\"/example/counter\\" hx-trigger=\\"button-click from:body\\" hx-swap=\\"outerHTML\\"><span>0</span><img class=\\"spinner htmx-indicator\\" src=\\"/public/images/spinner.svg\\"/></div><div class=\\"button-group\\"><button hx-target=\\"closest .button-group\\" hx-post=\\"/example/button\\" hx-swap=\\"outerHTML\\">Click me!</button></div></body></html>"`;
26 changes: 26 additions & 0 deletions src/views/connection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Html from '@kitajs/html'
import { singleton } from 'tsyringe'
import { Page } from './common'

interface connection {
company_name: string
status: 'pending' | 'unverified' | 'verified_them' | 'verified_us' | 'verified_both' | 'disconnected'
}

@singleton()
export default class ConnectionTemplates {
constructor() {}

public listPage = (connections: connection[]) => {
return (
<Page title="Veritable - Connections">
{connections.map((connection) => (
<div>
<div>{Html.escapeHtml(connection.company_name)}</div>
<div>{connection.status}</div>
</div>
))}
</Page>
)
}
}
3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"target": "ESNext",
"module": "CommonJS",
"moduleResolution": "node",
"typeRoots": ["types/*", "node_modules/@types"],
"forceConsistentCasingInFileNames": true,
"strict": true,
"sourceMap": true,
Expand All @@ -21,5 +20,5 @@
"resolveJsonModule": true
},
"include": ["src", "src/swagger.json"],
"exclude": ["**/__tests__"]
"exclude": ["test", "**/__tests__"]
}

0 comments on commit a53af57

Please sign in to comment.