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 { 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 })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
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 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)
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from './audio'
|
export * from './audio'
|
||||||
export * from './image'
|
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"
|
"outDir": "esm"
|
||||||
},
|
},
|
||||||
"references": [
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../test-utils"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,5 +5,8 @@
|
|||||||
"rootDir": "src"
|
"rootDir": "src"
|
||||||
},
|
},
|
||||||
"references": [
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../test-utils"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user