test: add authorize 100% coverage

This commit is contained in:
divlo 2020-12-29 04:05:39 +01:00
parent 0e534dd8ee
commit d44bd9e17e
6 changed files with 195 additions and 74 deletions

View File

@ -57,7 +57,9 @@
"jest": { "jest": {
"preset": "ts-jest", "preset": "ts-jest",
"testEnvironment": "node", "testEnvironment": "node",
"rootDir": "./src" "rootDir": "./src",
"collectCoverage": true,
"coverageDirectory": "../coverage/"
}, },
"ts-standard": { "ts-standard": {
"files": [ "files": [

16
src/UnauthorizedError.ts Normal file
View File

@ -0,0 +1,16 @@
export class UnauthorizedError extends Error {
public inner: { message: string }
public data: { message: string, code: string, type: 'UnauthorizedError' }
constructor (code: string, error: { message: string }) {
super(error.message)
this.message = error.message
this.inner = error
this.data = {
message: this.message,
code,
type: 'UnauthorizedError'
}
Object.setPrototypeOf(this, UnauthorizedError.prototype)
}
}

View File

@ -0,0 +1,69 @@
import axios from 'axios'
import { io } from 'socket.io-client'
import { fixtureStart, fixtureStop } from './fixture'
describe('authorize', () => {
let token: string = ''
beforeEach((done) => {
jest.setTimeout(15_000)
fixtureStart(async () => {
const response = await axios.post('http://localhost:9000/login')
token = response.data.token
done()
})
})
afterEach((done) => {
fixtureStop(done)
})
it('should emit error with no token provided', (done) => {
const socket = io('http://localhost:9000')
socket.on('connect_error', (err: any) => {
expect(err.data.message).toEqual('no token provided')
expect(err.data.code).toEqual('credentials_required')
socket.close()
done()
})
})
it('should emit error with bad token format', (done) => {
const socket = io('http://localhost:9000', {
extraHeaders: { Authorization: 'testing' }
})
socket.on('connect_error', (err: any) => {
expect(err.data.message).toEqual(
'Format is Authorization: Bearer [token]'
)
expect(err.data.code).toEqual('credentials_bad_format')
socket.close()
done()
})
})
it('should emit error with unauthorized handshake', (done) => {
const socket = io('http://localhost:9000', {
extraHeaders: { Authorization: 'Bearer testing' }
})
socket.on('connect_error', (err: any) => {
expect(err.data.message).toEqual(
'Unauthorized: Token is missing or invalid Bearer'
)
expect(err.data.code).toEqual('invalid_token')
socket.close()
done()
})
})
it('should connect the user', (done) => {
const socket = io('http://localhost:9000', {
extraHeaders: { Authorization: `Bearer ${token}` }
})
socket.on('connect', () => {
socket.close()
done()
})
})
})

View File

@ -0,0 +1,48 @@
import express from 'express'
import jwt from 'jsonwebtoken'
import { Server as HttpServer } from 'http'
import { Server as HttpsServer } from 'https'
import { Server as SocketIoServer } from 'socket.io'
import enableDestroy from 'server-destroy'
import { authorize } from '../../index'
interface Socket {
io: null | SocketIoServer
init: (httpServer: HttpServer | HttpsServer) => void
}
const socket: Socket = {
io: null,
init (httpServer) {
socket.io = new SocketIoServer(httpServer)
}
}
let server: HttpServer | null = null
export const fixtureStart = (done: any): void => {
const options = { secret: 'aaafoo super sercret' }
const app = express()
app.use(express.json())
app.post('/login', (_req, res) => {
const profile = {
email: 'john@doe.com',
id: 123
}
const token = jwt.sign(profile, options.secret, { expiresIn: 60 * 60 * 5 })
return res.json({ token })
})
server = app.listen(9000, done)
socket.init(server)
socket.io?.use(authorize(options))
enableDestroy(server)
}
export const fixtureStop = (callback: Function): void => {
socket.io?.close()
try {
server?.destroy()
} catch (err) {}
callback()
}

58
src/authorize.ts Normal file
View File

@ -0,0 +1,58 @@
import jwt from 'jsonwebtoken'
import { Socket } from 'socket.io'
import { UnauthorizedError } from './UnauthorizedError'
interface ExtendedError extends Error {
data?: any
}
type SocketIOMiddleware = (
socket: Socket,
next: (err?: ExtendedError) => void
) => void
interface AuthorizeOptions {
secret: string
}
export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => {
const { secret } = options
return (socket, next) => {
let token: string | null = null
const authorizationHeader = socket.request.headers.authorization
if (authorizationHeader != null) {
const tokenSplitted = authorizationHeader.split(' ')
if (tokenSplitted.length !== 2 || tokenSplitted[0] !== 'Bearer') {
return next(
new UnauthorizedError('credentials_bad_format', {
message: 'Format is Authorization: Bearer [token]'
})
)
}
token = tokenSplitted[1]
}
if (token == null) {
return next(
new UnauthorizedError('credentials_required', {
message: 'no token provided'
})
)
}
// Store encoded JWT
socket = Object.assign(socket, { encodedToken: token })
let payload: any
try {
payload = jwt.verify(token, secret)
} catch {
return next(
new UnauthorizedError('invalid_token', {
message: 'Unauthorized: Token is missing or invalid Bearer'
})
)
}
// Store decoded JWT
socket = Object.assign(socket, { decodedToken: payload })
return next()
}
}

View File

@ -1,73 +1 @@
import jwt from 'jsonwebtoken' export * from './authorize'
import { Socket } from 'socket.io'
class UnauthorizedError extends Error {
public inner: { message: string }
public data: { message: string, code: string, type: 'UnauthorizedError' }
constructor (code: string, error: { message: string }) {
super(error.message)
this.message = error.message
this.inner = error
this.data = {
message: this.message,
code,
type: 'UnauthorizedError'
}
Object.setPrototypeOf(this, UnauthorizedError.prototype)
}
}
interface ExtendedError extends Error {
data?: any
}
type SocketIOMiddleware = (
socket: Socket,
next: (err?: ExtendedError) => void
) => void
interface AuthorizeOptions {
secret: string
}
export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => {
const { secret } = options
return (socket, next) => {
let token: string | null = null
const authorizationHeader = socket.request.headers.authorization
if (authorizationHeader != null) {
const tokenSplitted = authorizationHeader.split(' ')
if (tokenSplitted.length !== 2 || tokenSplitted[0] !== 'Bearer') {
return next(
new UnauthorizedError('credentials_bad_format', {
message: 'Format is Authorization: Bearer [token]'
})
)
}
token = tokenSplitted[1]
}
if (token == null) {
return next(
new UnauthorizedError('credentials_required', {
message: 'no token provided'
})
)
}
// Store encoded JWT
socket = Object.assign(socket, { encodedToken: token })
let payload: any
try {
payload = jwt.verify(token, secret)
} catch {
return next(
new UnauthorizedError('invalid_token', {
message: 'Unauthorized: Token is missing or invalid Bearer'
})
)
}
// Store decoded JWT
socket = Object.assign(socket, { decodedToken: payload })
return next()
}
}