Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/connection controller #10

Merged
merged 4 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
mattdean-digicatapult marked this conversation as resolved.
Show resolved Hide resolved
})

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>
)
}
}
mattdean-digicatapult marked this conversation as resolved.
Show resolved Hide resolved
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__"]
}