Add tests packages/captcha
This commit is contained in:
parent
b6ad109aa0
commit
fe14691ac6
96
packages/captcha/src/audio.test.ts
Normal file
96
packages/captcha/src/audio.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@ -1,21 +1,40 @@
|
||||
import { spawn } from 'child_process'
|
||||
import { Request, Response } from 'express'
|
||||
import { Readable } from 'stream'
|
||||
import { run, ReadableProcess, ReadableWritable, Command } from './run'
|
||||
import SVGCaptcha from 'svg-captcha'
|
||||
|
||||
export async function audio(req: Request, res: Response) {
|
||||
// TODO generate random string
|
||||
const captcha = 'test'
|
||||
export interface AudioConfig {
|
||||
commands: Command[]
|
||||
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
|
||||
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)
|
||||
speech.stdout.pipe(res)
|
||||
}
|
||||
|
||||
async function speak(text: string) {
|
||||
const streams: ReadableWritable[] = [
|
||||
await espeak(),
|
||||
await opus(),
|
||||
]
|
||||
export async function speak(
|
||||
text: string,
|
||||
commands: Command[],
|
||||
): Promise<ReadableProcess> {
|
||||
const streams: ReadableWritable[] = []
|
||||
for (const command of commands) {
|
||||
streams.push(await run(command))
|
||||
}
|
||||
|
||||
const last = streams.reduce((prev, proc) => {
|
||||
prev.stdout.pipe(proc.stdin)
|
||||
@ -25,31 +44,6 @@ async function speak(text: string) {
|
||||
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 {
|
||||
constructor(text: string) {
|
||||
super()
|
||||
@ -65,20 +59,3 @@ function createTextStream(text: string): ReadableProcess {
|
||||
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 })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
21
packages/captcha/src/commands.test.ts
Normal file
21
packages/captcha/src/commands.test.ts
Normal 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',
|
||||
})
|
||||
})
|
||||
})
|
||||
22
packages/captcha/src/commands.ts
Normal file
22
packages/captcha/src/commands.ts
Normal 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',
|
||||
}
|
||||
}
|
||||
15
packages/captcha/src/espeak.ts
Normal file
15
packages/captcha/src/espeak.ts
Normal 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',
|
||||
}
|
||||
}
|
||||
39
packages/captcha/src/image.test.ts
Normal file
39
packages/captcha/src/image.test.ts
Normal 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))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@ -1,8 +1,14 @@
|
||||
import SVGCaptcha from 'svg-captcha'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
export function image(req: Request, res: Response) {
|
||||
const { text, data } = SVGCaptcha.create()
|
||||
export interface ImageConfig {
|
||||
size: number
|
||||
}
|
||||
|
||||
export const image = (config: ImageConfig) => (req: Request, res: Response) => {
|
||||
const { text, data } = SVGCaptcha.create({
|
||||
size: config.size,
|
||||
})
|
||||
req.session!.captcha = text
|
||||
res.type('svg')
|
||||
res.status(200)
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './audio'
|
||||
export * from './image'
|
||||
export * from './commands'
|
||||
|
||||
43
packages/captcha/src/run.test.ts
Normal file
43
packages/captcha/src/run.test.ts
Normal 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/)
|
||||
})
|
||||
|
||||
})
|
||||
35
packages/captcha/src/run.ts
Normal file
35
packages/captcha/src/run.ts
Normal 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 })
|
||||
}
|
||||
})
|
||||
}
|
||||
4
packages/captcha/src/testProcess.ts
Normal file
4
packages/captcha/src/testProcess.ts
Normal file
@ -0,0 +1,4 @@
|
||||
process.stdin.on('data', data => {
|
||||
process.stdout.write(data)
|
||||
process.exit(0)
|
||||
})
|
||||
@ -4,5 +4,8 @@
|
||||
"outDir": "esm"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../test-utils"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -5,5 +5,8 @@
|
||||
"rootDir": "src"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../test-utils"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user