Add tests packages/captcha

This commit is contained in:
Jerko Steiner 2019-10-31 15:05:36 -04:00
parent b6ad109aa0
commit fe14691ac6
13 changed files with 319 additions and 54 deletions

View File

@ -0,0 +1,96 @@
import express from 'express'
import session from 'express-session'
import cookieParser from 'cookie-parser'
import request from 'supertest'
import { audio, speak } from './audio'
import { join } from 'path'
describe('speak', () => {
it('writes speech data to stdin and returns rw streams', async () => {
async function read(readable: NodeJS.ReadableStream): Promise<string> {
return new Promise((resolve, reject) => {
readable.on('error', err => reject(err))
readable.on('readable', () => {
let data = ''
let chunk
while (null !== (chunk = readable.read())) {
data += chunk
}
resolve(data)
})
})
}
const command = {
cmd: process.argv[0],
args: [join(__dirname, 'testProcess.ts')],
contentType: 'text/plain',
}
const rw = await speak('mytest', [command])
const data = await read(rw.stdout)
expect(data).toEqual('mytest')
})
})
describe('audio', () => {
const app = express()
app.use(cookieParser())
app.use(session({
saveUninitialized: false,
resave: false,
secret: 'test',
}))
app.get('/captcha/audio', audio({
commands: [{
cmd: process.argv[0],
args: [join(__dirname, 'testProces.ts')],
contentType: 'text/plain',
}],
size: 6,
}))
app.get('/captcha/session', (req, res) => {
res.json(req.session)
})
describe('/captcha/audio', () => {
it('generates a new captcha', async () => {
const res1 = await request(app)
.get('/captcha/audio')
.expect('Content-type', /text\/plain/)
.expect(200)
const cookie = res1.header['set-cookie'][0]
const res2 = await request(app)
.get('/captcha/session')
.set('cookie', cookie)
.expect(200)
expect(res2.body.captcha).toEqual(jasmine.any(String))
})
it('fails with error 500 when unable to generate', async () => {
const app = express()
app.use(cookieParser())
app.use(session({
saveUninitialized: false,
resave: false,
secret: 'test',
}))
app.get('/captcha/audio', audio({
commands: [{
cmd: 'non-existing-command',
args: [],
contentType: 'text/plain',
}],
size: 6,
}))
await request(app)
.get('/captcha/audio')
.expect(500)
.expect('Internal server error')
})
})
})

View File

@ -1,21 +1,40 @@
import { spawn } from 'child_process'
import { Request, Response } from 'express' import { Request, Response } from 'express'
import { Readable } from 'stream' import { Readable } from 'stream'
import { run, ReadableProcess, ReadableWritable, Command } from './run'
import SVGCaptcha from 'svg-captcha'
export async function audio(req: Request, res: Response) { export interface AudioConfig {
// TODO generate random string commands: Command[]
const captcha = 'test' size: number
}
export const audio = (config: AudioConfig) => async (
req: Request,
res: Response,
) => {
const { commands, size } = config
const captcha = SVGCaptcha.randomText(size)
req.session!.captcha = captcha req.session!.captcha = captcha
const speech = await speak('test') let speech: ReadableProcess
try {
speech = await speak('test', commands)
} catch (err) {
res.status(500)
res.send('Internal server error')
return
}
res.type(speech.contentType) res.type(speech.contentType)
speech.stdout.pipe(res) speech.stdout.pipe(res)
} }
async function speak(text: string) { export async function speak(
const streams: ReadableWritable[] = [ text: string,
await espeak(), commands: Command[],
await opus(), ): Promise<ReadableProcess> {
] const streams: ReadableWritable[] = []
for (const command of commands) {
streams.push(await run(command))
}
const last = streams.reduce((prev, proc) => { const last = streams.reduce((prev, proc) => {
prev.stdout.pipe(proc.stdin) prev.stdout.pipe(proc.stdin)
@ -25,31 +44,6 @@ async function speak(text: string) {
return last return last
} }
interface ReadableProcess {
stdout: NodeJS.ReadableStream
contentType: string
}
interface WritableProcess {
stdin: NodeJS.WritableStream
}
interface ReadableWritable extends ReadableProcess, WritableProcess {
}
function espeak() {
return run(
'espeak',
['-k', '2', '-s', '90', '--stdin', '--stdout'],
'audio/wav',
)
}
async function opus() {
return run('opusenc', ['-', '-'], 'audio/opus')
}
class TextStream extends Readable { class TextStream extends Readable {
constructor(text: string) { constructor(text: string) {
super() super()
@ -65,20 +59,3 @@ function createTextStream(text: string): ReadableProcess {
contentType: 'text/plain', contentType: 'text/plain',
} }
} }
async function run(
cmd: string, args: string[], contentType: string,
): Promise<ReadableWritable> {
return new Promise((resolve, reject) => {
const p = spawn(cmd, args)
p.once('error', err => {
console.error(err.stack)
reject(err)
})
if (p.pid) {
resolve({ stdin: p.stdin, stdout: p.stdout, contentType })
}
})
}

View File

@ -0,0 +1,21 @@
import { espeak, opusenc } from './commands'
describe('espeak', () => {
it('returns espeak arguments', () => {
expect(espeak({})).toEqual({
cmd: 'espeak',
args: ['-k', '2', '-s', '90', '--stdin', '--stdout'],
contentType: 'audio/wav',
})
})
})
describe('opus', () => {
it('returns opusenc arguments', () => {
expect(opusenc({})).toEqual({
cmd: 'opusenc',
args: ['-', '-'],
contentType: 'audio/opus',
})
})
})

View File

@ -0,0 +1,22 @@
export interface ESpeakOptions {
}
export interface OpusOptions {
}
export function espeak(config: ESpeakOptions) {
return {
cmd: 'espeak',
args: ['-k', '2', '-s', '90', '--stdin', '--stdout'],
contentType: 'audio/wav',
}
}
export function opusenc(config: OpusOptions) {
return {
cmd: 'opusenc',
args: ['-', '-'],
contentType: 'audio/opus',
}
}

View File

@ -0,0 +1,15 @@
export function espeak() {
return {
cmd: 'espeak',
args: ['-k', '2', '-s', '90', '--stdin', '--stdout'],
contentType: 'audio/wav',
}
}
export function opus() {
return {
cmd: 'opusenc',
args: ['-', '-'],
contentType: 'audio/opus',
}
}

View File

@ -0,0 +1,39 @@
import express from 'express'
import session from 'express-session'
import cookieParser from 'cookie-parser'
import request from 'supertest'
import { image } from './image'
describe('image', () => {
const app = express()
app.use(cookieParser())
app.use(session({
saveUninitialized: false,
resave: false,
secret: 'test',
}))
app.get('/captcha/image', image({ size: 6 }))
app.get('/captcha/session', (req, res) => {
res.json(req.session)
})
describe('/captcha/image', () => {
it('generates a new captcha', async () => {
const res1 = await request(app)
.get('/captcha/image')
.expect('Content-type', /svg/)
.expect(200)
const cookie = res1.header['set-cookie'][0]
const res2 = await request(app)
.get('/captcha/session')
.set('cookie', cookie)
.expect(200)
expect(res2.body.captcha).toEqual(jasmine.any(String))
})
})
})

View File

@ -1,8 +1,14 @@
import SVGCaptcha from 'svg-captcha' import SVGCaptcha from 'svg-captcha'
import { Request, Response } from 'express' import { Request, Response } from 'express'
export function image(req: Request, res: Response) { export interface ImageConfig {
const { text, data } = SVGCaptcha.create() size: number
}
export const image = (config: ImageConfig) => (req: Request, res: Response) => {
const { text, data } = SVGCaptcha.create({
size: config.size,
})
req.session!.captcha = text req.session!.captcha = text
res.type('svg') res.type('svg')
res.status(200) res.status(200)

View File

@ -1,2 +1,3 @@
export * from './audio' export * from './audio'
export * from './image' export * from './image'
export * from './commands'

View File

@ -0,0 +1,43 @@
import { run } from './run'
import { getError } from '@rondo.dev/test-utils'
import { join } from 'path'
describe('run', () => {
async function read(readable: NodeJS.ReadableStream): Promise<string> {
return new Promise((resolve, reject) => {
readable.on('error', err => reject(err))
readable.on('readable', () => {
let data = ''
let chunk
while (null !== (chunk = readable.read())) {
data += chunk
}
resolve(data)
})
})
}
it('runs a process and returns stdin/stdout/contentType', async () => {
const result = await run({
cmd: process.argv[0],
args: [join(__dirname, 'testProcess.ts')],
contentType: 'text/plain',
})
expect(result.contentType).toBe('text/plain')
result.stdin.write('test')
const text = await read(result.stdout)
expect(text.trim()).toBe('test')
})
it('rejects when command is invalid', async () => {
const error = await getError(run({
cmd: 'invalid-command',
args: ['test'],
contentType: 'text/plain',
}))
expect(error.message).toMatch(/ENOENT/)
})
})

View File

@ -0,0 +1,35 @@
import { spawn } from 'child_process'
export interface ReadableProcess {
stdout: NodeJS.ReadableStream
contentType: string
}
export interface WritableProcess {
stdin: NodeJS.WritableStream
}
export interface ReadableWritable extends ReadableProcess, WritableProcess {
}
export interface Command {
cmd: string
args: string[]
contentType: string
}
export async function run(command: Command): Promise<ReadableWritable> {
const { cmd, args, contentType } = command
return new Promise((resolve, reject) => {
const p = spawn(cmd, args)
p.once('error', err => {
reject(err)
})
if (p.pid) {
resolve({ stdin: p.stdin, stdout: p.stdout, contentType })
}
})
}

View File

@ -0,0 +1,4 @@
process.stdin.on('data', data => {
process.stdout.write(data)
process.exit(0)
})

View File

@ -4,5 +4,8 @@
"outDir": "esm" "outDir": "esm"
}, },
"references": [ "references": [
{
"path": "../test-utils"
}
] ]
} }

View File

@ -5,5 +5,8 @@
"rootDir": "src" "rootDir": "src"
}, },
"references": [ "references": [
{
"path": "../test-utils"
}
] ]
} }