test: add authorize 100% coverage
This commit is contained in:
parent
0e534dd8ee
commit
d44bd9e17e
@ -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
16
src/UnauthorizedError.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
69
src/__test__/authorize.test.ts
Normal file
69
src/__test__/authorize.test.ts
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
48
src/__test__/fixture/index.ts
Normal file
48
src/__test__/fixture/index.ts
Normal 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
58
src/authorize.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
74
src/index.ts
74
src/index.ts
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user