@pbvision/fastify-firestore-service 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +204 -0
- package/README.md +193 -0
- package/docs/api.md +695 -0
- package/docs/build.sh +24 -0
- package/docs/jsdoc.config.json +22 -0
- package/package.json +90 -0
- package/src/api/api.js +790 -0
- package/src/api/db-api.js +108 -0
- package/src/api/exception.js +288 -0
- package/src/api/response.js +8 -0
- package/src/component-registrar.js +27 -0
- package/src/got-wrapper.js +34 -0
- package/src/index.js +12 -0
- package/src/make-app.js +179 -0
- package/src/make-logger.js +119 -0
- package/src/plugins/compress.js +28 -0
- package/src/plugins/content-parser.js +21 -0
- package/src/plugins/cookie.js +17 -0
- package/src/plugins/error-handler.js +140 -0
- package/src/plugins/health-check.js +19 -0
- package/src/plugins/latency-tracker.js +45 -0
- package/src/plugins/swagger.js +50 -0
- package/test/base-test.js +154 -0
- package/test/environment.js +3 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// istanbul ignore file
|
|
2
|
+
import querystring from 'node:querystring'
|
|
3
|
+
|
|
4
|
+
import pino from 'pino'
|
|
5
|
+
import wrap from 'word-wrap'
|
|
6
|
+
|
|
7
|
+
// for unit testing, output to the console INSTEAD of to stdout
|
|
8
|
+
// via pino (so that jest captures the output and groups it with the right test
|
|
9
|
+
// suite)
|
|
10
|
+
function getUnitTestLogFormatOverrides () {
|
|
11
|
+
const prefixText = '/' + process.env.SERVICE + '/src/'
|
|
12
|
+
function prettifier () {
|
|
13
|
+
return (obj) => {
|
|
14
|
+
if (obj.req) {
|
|
15
|
+
const indent = ''
|
|
16
|
+
if (!obj.status) {
|
|
17
|
+
console.log(indent, obj.req.method, obj.req.path)
|
|
18
|
+
} else {
|
|
19
|
+
// output the status code and (if any) error message
|
|
20
|
+
const msgs = wrap(obj.msg || '', { width: 80, indent }).split('\n')
|
|
21
|
+
console.log(indent, ' \u2514 HTTP', obj.status, msgs[0])
|
|
22
|
+
msgs.slice(1).forEach(msg => {
|
|
23
|
+
console.log(indent, msg)
|
|
24
|
+
})
|
|
25
|
+
if (obj.error && obj.status >= 500) {
|
|
26
|
+
console.log(`${indent}Error: ${JSON.stringify(obj.error, null, 2)}`)
|
|
27
|
+
}
|
|
28
|
+
if (obj.stack) {
|
|
29
|
+
for (let i = 0; i < obj.stack.length; i++) {
|
|
30
|
+
console.log(' ', obj.stack[i])
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
// 1) Get the filename and line number the log message is from
|
|
36
|
+
// the index of the first non-pino line may change in future versions
|
|
37
|
+
// of fastify or pino; might be better to search each line until we
|
|
38
|
+
// find /<service>/src (if present)
|
|
39
|
+
const firstNonPinoLine = (new Error()).stack.split('\n')[5]
|
|
40
|
+
const appFolderIdx = firstNonPinoLine.indexOf(prefixText)
|
|
41
|
+
let prefix = ''
|
|
42
|
+
if (appFolderIdx !== -1) {
|
|
43
|
+
// prefix log message with filename and line number when logs
|
|
44
|
+
// originated in our app's source code
|
|
45
|
+
prefix = firstNonPinoLine.substring(
|
|
46
|
+
appFolderIdx + 9,
|
|
47
|
+
firstNonPinoLine.lastIndexOf(':')) + ' '
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2) Create the logm msg: file, line, log message and log object
|
|
51
|
+
const indent = ' \u2502 '
|
|
52
|
+
const logObj = {}
|
|
53
|
+
const ignoredKeys = ['level', 'msg', 'reqId', 'time', 'v']
|
|
54
|
+
Object.getOwnPropertyNames(obj).forEach(key => {
|
|
55
|
+
if (ignoredKeys.indexOf(key) === -1) {
|
|
56
|
+
logObj[key] = obj[key]
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
let msg = indent + prefix + (obj.msg || '')
|
|
60
|
+
if (Object.keys(logObj).length) {
|
|
61
|
+
msg += (obj.msg ? ' ' : '') + JSON.stringify(logObj)
|
|
62
|
+
}
|
|
63
|
+
if (obj.level < 30) {
|
|
64
|
+
console.debug(msg)
|
|
65
|
+
} else if (obj.level >= 50) {
|
|
66
|
+
console.error(msg)
|
|
67
|
+
} else if (obj.level >= 40) {
|
|
68
|
+
console.warn(msg)
|
|
69
|
+
} else {
|
|
70
|
+
console.log(msg)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return '' // skip pino logging
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
prettifier,
|
|
78
|
+
transport: {
|
|
79
|
+
target: 'pino-pretty',
|
|
80
|
+
options: {
|
|
81
|
+
colorize: true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default function makeCustomLogger (useUnitTestLogFormat) {
|
|
88
|
+
function serializeReq (req) {
|
|
89
|
+
const q = req.query
|
|
90
|
+
if (req.raw) {
|
|
91
|
+
req = req.raw
|
|
92
|
+
}
|
|
93
|
+
const path = req.path
|
|
94
|
+
const qs = req.qs
|
|
95
|
+
return {
|
|
96
|
+
app: req.headers['x-app'] || '',
|
|
97
|
+
uid: req.headers['x-uid'] || '',
|
|
98
|
+
method: req.method,
|
|
99
|
+
ua: req.headers['user-agent'] || '',
|
|
100
|
+
path,
|
|
101
|
+
q: q || querystring.decode(qs)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const options = {
|
|
105
|
+
base: null, // omit pino default fields like pid and hostname
|
|
106
|
+
level: (process.env.NODE_ENV === 'prod') ? 'info' : 'debug',
|
|
107
|
+
serializers: {
|
|
108
|
+
req: serializeReq,
|
|
109
|
+
res: (res) => {
|
|
110
|
+
return { status: res.statusCode, req: serializeReq(res.req) }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// on localhost, we customize the logs to optimize for console-based debugging
|
|
115
|
+
if (useUnitTestLogFormat) {
|
|
116
|
+
Object.assign(options, getUnitTestLogFormatOverrides())
|
|
117
|
+
}
|
|
118
|
+
return pino(options)
|
|
119
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// eslint babel made changes to chained expressions which is breaking ast-types
|
|
2
|
+
// https://github.com/babel/babel/issues/11908
|
|
3
|
+
|
|
4
|
+
// ast-types has a fix, but it is not yet in a versioned release
|
|
5
|
+
// https://github.com/benjamn/ast-types/pull/399
|
|
6
|
+
|
|
7
|
+
// to circumvent problems caused by importing zlib, we do it in a separate file
|
|
8
|
+
// so we can ignore it in linting
|
|
9
|
+
import zlib from 'node:zlib'
|
|
10
|
+
|
|
11
|
+
import { fastifyCompress } from '@fastify/compress'
|
|
12
|
+
import fp from 'fastify-plugin'
|
|
13
|
+
|
|
14
|
+
// TODO: fine tune configurations once we have a reliable usage pattern
|
|
15
|
+
export default fp(function (fastify, options, next) {
|
|
16
|
+
fastify.register(fastifyCompress, {
|
|
17
|
+
threshold: 20,
|
|
18
|
+
brotliOptions: {
|
|
19
|
+
params: {
|
|
20
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 4
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
next()
|
|
25
|
+
}, {
|
|
26
|
+
fastify: '>=3.x',
|
|
27
|
+
name: 'compress'
|
|
28
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fp from 'fastify-plugin'
|
|
2
|
+
|
|
3
|
+
function addContentParser (fastify, options, next) {
|
|
4
|
+
fastify.addContentTypeParser('application/json',
|
|
5
|
+
{ parseAs: 'string' },
|
|
6
|
+
function (req, body, done) {
|
|
7
|
+
try {
|
|
8
|
+
const json = JSON.parse(body || '{}')
|
|
9
|
+
done(null, json)
|
|
10
|
+
} catch (err) {
|
|
11
|
+
err.statusCode = 400
|
|
12
|
+
done(err, undefined)
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
next()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default fp(addContentParser, {
|
|
19
|
+
fastify: '>=3.x',
|
|
20
|
+
name: 'content-parser'
|
|
21
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import cookiePlugin from '@fastify/cookie'
|
|
2
|
+
import fp from 'fastify-plugin'
|
|
3
|
+
|
|
4
|
+
function addCookiePlugin (fastify, options, next) {
|
|
5
|
+
// istanbul ignore if
|
|
6
|
+
if (options.cookie.disabled) {
|
|
7
|
+
next()
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
fastify.register(cookiePlugin, { secret: options.cookie.secret })
|
|
11
|
+
next()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default fp(addCookiePlugin, {
|
|
15
|
+
fastify: '>=3.x',
|
|
16
|
+
name: 'content-parser'
|
|
17
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import S from '@pbvision/schema'
|
|
2
|
+
import * as Sentry from '@sentry/node'
|
|
3
|
+
import fp from 'fastify-plugin'
|
|
4
|
+
|
|
5
|
+
import { InvalidInputException } from '../api/exception.js'
|
|
6
|
+
|
|
7
|
+
export default fp(function (fastify, options, next) {
|
|
8
|
+
const isLocalhost = process.env.NODE_ENV === 'localhost'
|
|
9
|
+
const sentryDSN = options.errorHandler.sentryDSN
|
|
10
|
+
// istanbul ignore next
|
|
11
|
+
const isSentryEnabled = sentryDSN && !isLocalhost
|
|
12
|
+
Sentry.init({
|
|
13
|
+
dsn: sentryDSN,
|
|
14
|
+
enabled: isSentryEnabled,
|
|
15
|
+
environment: process.env.NODE_ENV,
|
|
16
|
+
release: process.env.GIT_HASH,
|
|
17
|
+
serverName: options.errorHandler.serverName
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const returnErrorDetail = options.errorHandler.returnErrorDetail
|
|
21
|
+
// log any exception which occurs
|
|
22
|
+
fastify.setErrorHandler(async (error, req, reply) => {
|
|
23
|
+
// extract the relevant bit of the traceback: remove fastify lines
|
|
24
|
+
const traceback = error.stack.split('\n')
|
|
25
|
+
const errorMessage = error.message.split('\n')
|
|
26
|
+
traceback.splice(0, errorMessage.length)
|
|
27
|
+
let removeFromIdx
|
|
28
|
+
if (error instanceof InvalidInputException) {
|
|
29
|
+
removeFromIdx = 1
|
|
30
|
+
} else {
|
|
31
|
+
for (let i = traceback.length - 1; i > 0; i--) {
|
|
32
|
+
const tbLine = traceback[i]
|
|
33
|
+
if (tbLine.indexOf('/fastify/lib') !== -1) {
|
|
34
|
+
removeFromIdx = i + 1
|
|
35
|
+
break
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
removeFromIdx = removeFromIdx ?? traceback.length
|
|
40
|
+
|
|
41
|
+
traceback.splice(removeFromIdx, traceback.length - removeFromIdx)
|
|
42
|
+
|
|
43
|
+
const response = reply.raw
|
|
44
|
+
/* istanbul ignore next */
|
|
45
|
+
const message = error.message || 'empty error message'
|
|
46
|
+
const statusCode = error.httpCode ?? error.statusCode ?? 500
|
|
47
|
+
reply.code(statusCode)
|
|
48
|
+
const errInfo = {
|
|
49
|
+
msg: message,
|
|
50
|
+
req,
|
|
51
|
+
status: statusCode,
|
|
52
|
+
stack: traceback
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// improve the error emitted from bad requests (invalid input)
|
|
56
|
+
const isCrash = errInfo.status >= 500
|
|
57
|
+
let customFingerprint = false
|
|
58
|
+
if (!isCrash) {
|
|
59
|
+
const firstTB = traceback[0]
|
|
60
|
+
/* istanbul ignore else */
|
|
61
|
+
if (firstTB) {
|
|
62
|
+
/* istanbul ignore else */
|
|
63
|
+
if (firstTB.indexOf('fastify/lib/contentTypeParser.js') !== -1) {
|
|
64
|
+
customFingerprint = 'Content-Type Not Permitted'
|
|
65
|
+
} else if (error instanceof S.ValidationError) {
|
|
66
|
+
customFingerprint = message
|
|
67
|
+
}
|
|
68
|
+
/* istanbul ignore next */
|
|
69
|
+
if (customFingerprint) {
|
|
70
|
+
if (customFingerprint.indexOf(errInfo.msg) === -1) {
|
|
71
|
+
// prefix the error message with the custom fingerprint text if the
|
|
72
|
+
// fingerprint didn't already contain all of the error message text
|
|
73
|
+
errInfo.msg = customFingerprint + ': ' + errInfo.msg
|
|
74
|
+
}
|
|
75
|
+
// changing the error name results in a cleaner description on the
|
|
76
|
+
// Sentry dashboard
|
|
77
|
+
error.name = customFingerprint
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Object.getOwnPropertyNames(error).forEach(key => {
|
|
83
|
+
if (key !== 'stack' && key !== 'message') {
|
|
84
|
+
if (!errInfo.error) {
|
|
85
|
+
errInfo.error = {}
|
|
86
|
+
}
|
|
87
|
+
errInfo.error[key] = error[key]
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
response.logged = true // don't double-log
|
|
91
|
+
if (statusCode >= 500) {
|
|
92
|
+
reply.log.error(errInfo)
|
|
93
|
+
} else {
|
|
94
|
+
reply.log.info(errInfo)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Sentry.withScope(function (scope) {
|
|
98
|
+
if (customFingerprint) {
|
|
99
|
+
scope.setFingerprint(customFingerprint)
|
|
100
|
+
}
|
|
101
|
+
const user = {}
|
|
102
|
+
// istanbul ignore if
|
|
103
|
+
if (req.headers['x-uid']) {
|
|
104
|
+
user.id = req.headers['x-uid']
|
|
105
|
+
} else {
|
|
106
|
+
user.ip = req.ip
|
|
107
|
+
}
|
|
108
|
+
scope.setLevel(isCrash ? 'error' : 'warning')
|
|
109
|
+
scope.setUser(user)
|
|
110
|
+
scope.setTags({
|
|
111
|
+
method: req.method,
|
|
112
|
+
url: req.url,
|
|
113
|
+
status: errInfo.status
|
|
114
|
+
})
|
|
115
|
+
scope.setExtras({
|
|
116
|
+
msg: errInfo.message,
|
|
117
|
+
reqId: req.id,
|
|
118
|
+
userAgent: req.headers['user-agent'] ?? 'not set'
|
|
119
|
+
})
|
|
120
|
+
Sentry.captureException(error)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const errorData = error.respData ?? {
|
|
124
|
+
code: error.constructor.name,
|
|
125
|
+
message: customFingerprint || error.message
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// istanbul ignore else
|
|
129
|
+
if (returnErrorDetail && !error.respData) {
|
|
130
|
+
errorData.detail = errInfo.msg
|
|
131
|
+
errorData.stack = errInfo.stack
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await reply.header('Content-Type', 'application/json; charset=utf-8')
|
|
135
|
+
.serializer(o => JSON.stringify(o, null, 2))
|
|
136
|
+
.send(errorData)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
next()
|
|
140
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fp from 'fastify-plugin'
|
|
2
|
+
|
|
3
|
+
export default fp(function (fastify, options, next) {
|
|
4
|
+
// istanbul ignore if
|
|
5
|
+
if (options.healthCheck.disabled) {
|
|
6
|
+
next()
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
// istanbul ignore next
|
|
10
|
+
const path = options.healthCheck.path ?? '/'
|
|
11
|
+
fastify.get(path, { schema: { hide: true } },
|
|
12
|
+
async (req, reply) => {
|
|
13
|
+
await reply.send()
|
|
14
|
+
})
|
|
15
|
+
next()
|
|
16
|
+
}, {
|
|
17
|
+
fastify: '>=3.x',
|
|
18
|
+
name: 'healthCheck'
|
|
19
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fp from 'fastify-plugin'
|
|
2
|
+
const symbolRequestTime = Symbol('RequestTimer')
|
|
3
|
+
|
|
4
|
+
export default fp(function (fastify, options, next) {
|
|
5
|
+
// istanbul ignore if
|
|
6
|
+
if (options.latencyTracker.disabled) {
|
|
7
|
+
next()
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const header = options.latencyTracker.header
|
|
12
|
+
fastify.addHook('onRequest', function onRequestHandler (req, reply, done) {
|
|
13
|
+
// Start the recording of process time
|
|
14
|
+
req.raw[symbolRequestTime] = process.hrtime.bigint()
|
|
15
|
+
|
|
16
|
+
done()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
fastify.addHook('onSend', async (req, reply, payload) => {
|
|
20
|
+
const fixedDigits = 3
|
|
21
|
+
// Calculate the duration, in nanoseconds
|
|
22
|
+
const hrDuration = process.hrtime.bigint() - req.raw[symbolRequestTime]
|
|
23
|
+
// convert it to milliseconds
|
|
24
|
+
const duration = (Number(hrDuration) / 1e6).toFixed(fixedDigits)
|
|
25
|
+
// add the header to the response
|
|
26
|
+
reply.header(header, duration)
|
|
27
|
+
return payload
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
fastify.addHook('onResponse', (req, reply, done) => {
|
|
31
|
+
// if errored, then it was already logged
|
|
32
|
+
if (!reply.raw.logged) {
|
|
33
|
+
req.log.info({
|
|
34
|
+
req,
|
|
35
|
+
status: reply.raw.statusCode,
|
|
36
|
+
latency: reply.getHeader(header)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
done()
|
|
40
|
+
})
|
|
41
|
+
next()
|
|
42
|
+
}, {
|
|
43
|
+
fastify: '>=3.x',
|
|
44
|
+
name: 'latency-tracker'
|
|
45
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// istanbul ignore file
|
|
2
|
+
import swagger from '@fastify/swagger'
|
|
3
|
+
import swaggerUI from '@fastify/swagger-ui'
|
|
4
|
+
import fp from 'fastify-plugin'
|
|
5
|
+
|
|
6
|
+
export default fp(function (fastify, options, next) {
|
|
7
|
+
if (options.swagger.disabled) {
|
|
8
|
+
next()
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const authHeaders = options.swagger.authHeaders
|
|
13
|
+
fastify.register(swagger, {
|
|
14
|
+
openapi: {
|
|
15
|
+
servers: options.swagger.servers.map(x => { return { url: x } }),
|
|
16
|
+
info: {
|
|
17
|
+
title: `${options.swagger.service.toUpperCase()} Service`
|
|
18
|
+
},
|
|
19
|
+
consumes: ['application/json'],
|
|
20
|
+
produces: ['application/json'],
|
|
21
|
+
components: {
|
|
22
|
+
securitySchemes: authHeaders.reduce((all, c) => {
|
|
23
|
+
all[c.replace('x-', '')] = {
|
|
24
|
+
type: 'apiKey',
|
|
25
|
+
name: c,
|
|
26
|
+
in: 'header'
|
|
27
|
+
}
|
|
28
|
+
return all
|
|
29
|
+
}, {})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
fastify.register(swaggerUI, {
|
|
34
|
+
routePrefix: options.swagger.routePrefix,
|
|
35
|
+
exposeRoute: true,
|
|
36
|
+
uiConfig: {
|
|
37
|
+
docExpansion: 'list',
|
|
38
|
+
deepLinking: false,
|
|
39
|
+
filter: true
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
// fastify.addHook('onReady', function (done) {
|
|
43
|
+
// fastify.swagger()
|
|
44
|
+
// done()
|
|
45
|
+
// })
|
|
46
|
+
next()
|
|
47
|
+
}, {
|
|
48
|
+
fastify: '>=3.x',
|
|
49
|
+
name: 'swagger'
|
|
50
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import zlib from 'node:zlib'
|
|
2
|
+
|
|
3
|
+
import { jest } from '@jest/globals'
|
|
4
|
+
import { BaseTest, runTests } from '@pbvision/jest-unit-test'
|
|
5
|
+
import superagentDefaults from 'superagent-defaults'
|
|
6
|
+
import supertest from 'supertest'
|
|
7
|
+
|
|
8
|
+
import makeService from '../src/app.js'
|
|
9
|
+
|
|
10
|
+
let FASTIFY_CACHE
|
|
11
|
+
|
|
12
|
+
afterAll(async () => {
|
|
13
|
+
await FASTIFY_CACHE?.close()
|
|
14
|
+
FASTIFY_CACHE = undefined
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
class BaseAppTest extends BaseTest {
|
|
18
|
+
async beforeAll () {
|
|
19
|
+
this.fastify = FASTIFY_CACHE ?? await makeService()
|
|
20
|
+
FASTIFY_CACHE = this.fastify
|
|
21
|
+
|
|
22
|
+
await Promise.all([super.beforeAll(), this.fastify.ready()])
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Avoid having to consider compression in unit tests
|
|
26
|
+
* by removing the `accept-encoding` header,
|
|
27
|
+
* which is added by default from SuperTest
|
|
28
|
+
*/
|
|
29
|
+
const superTest = superagentDefaults(supertest(this.fastify.server))
|
|
30
|
+
superTest.set('accept-encoding', null)
|
|
31
|
+
this.app = new Proxy(superTest, {
|
|
32
|
+
get: (target, prop, receiver) => {
|
|
33
|
+
if (['get', 'post', 'put', 'delete'].includes(prop)) {
|
|
34
|
+
return (...requestParams) => {
|
|
35
|
+
const test = target[prop](...requestParams)
|
|
36
|
+
const originalExpect = test.expect
|
|
37
|
+
test.expect = async (...expectParams) => {
|
|
38
|
+
console.log(' \u2502 Expecting', expectParams[0])
|
|
39
|
+
return originalExpect.call(test, ...expectParams)
|
|
40
|
+
}
|
|
41
|
+
return test
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
return target[prop]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// the promise input conveniently matches the promise produced by supertest
|
|
52
|
+
// so you can pass the output of app.post(), etc. as the promise here as is
|
|
53
|
+
function makeGotMockValueFromPromise (promise) {
|
|
54
|
+
const mockValue = new Promise(resolve => {
|
|
55
|
+
promise.then(desiredHttpResponse => {
|
|
56
|
+
let body = desiredHttpResponse.text || desiredHttpResponse.body || ''
|
|
57
|
+
if (typeof body !== 'string') {
|
|
58
|
+
body = JSON.stringify(body)
|
|
59
|
+
}
|
|
60
|
+
mockValue.text = async () => body
|
|
61
|
+
resolve({ statusCode: desiredHttpResponse.status || 200 })
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
return mockValue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeGotMockValue (body, statusCode, callback) {
|
|
68
|
+
const mockValue = new Promise(resolve => {
|
|
69
|
+
// setTimeout is used so that this promise does not synchronously resolve
|
|
70
|
+
// because unmocked got will NEVER return synchronously. This ensures
|
|
71
|
+
// functions which call got() never resolve synchronously (which can
|
|
72
|
+
// change their behavior... e.g., allow them to throw when called,
|
|
73
|
+
// instead of only rejecting later when await'ed).
|
|
74
|
+
// https://github.com/facebook/jest/issues/6028 (since jest 21.x)
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
mockValue.text = async () => {
|
|
77
|
+
if (callback) {
|
|
78
|
+
callback()
|
|
79
|
+
}
|
|
80
|
+
if (typeof body === 'string') {
|
|
81
|
+
return body
|
|
82
|
+
}
|
|
83
|
+
return JSON.stringify(body)
|
|
84
|
+
}
|
|
85
|
+
resolve({ statusCode })
|
|
86
|
+
}, 0)
|
|
87
|
+
})
|
|
88
|
+
mockValue.text = async () => {
|
|
89
|
+
throw new Error('cannot use text() before calling await() on the response')
|
|
90
|
+
}
|
|
91
|
+
return mockValue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mockGot () {
|
|
95
|
+
const gotMock = jest.fn().mockImplementation(({ body }) => {
|
|
96
|
+
expect(body).toEqual(zlib.brotliCompressSync('321'))
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
gotMock.mockResp = (body = '', statusCode = 200, callback) => {
|
|
100
|
+
gotMock.mockReturnValue(makeGotMockValue(body, statusCode, callback))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Determine the mock response to use when the request is made.
|
|
105
|
+
*
|
|
106
|
+
* @param {...function (request)} callbacks a list of callbacks in the order
|
|
107
|
+
* to check and see if they have a mock response to use; if no callback,
|
|
108
|
+
* provides a mock response then an error will be thrown
|
|
109
|
+
*/
|
|
110
|
+
gotMock.mockRespWithCallback = (...callbacks) => {
|
|
111
|
+
gotMock.mockImplementation(request => {
|
|
112
|
+
for (const callback of callbacks) {
|
|
113
|
+
const desiredHTTPResponse = callback(request)
|
|
114
|
+
if (desiredHTTPResponse === true) {
|
|
115
|
+
const unmockedGot = jest.requireActual('../src/got')
|
|
116
|
+
return unmockedGot(request)
|
|
117
|
+
}
|
|
118
|
+
if (desiredHTTPResponse) {
|
|
119
|
+
if (desiredHTTPResponse.then) {
|
|
120
|
+
return makeGotMockValueFromPromise(desiredHTTPResponse)
|
|
121
|
+
}
|
|
122
|
+
return makeGotMockValue(
|
|
123
|
+
// follows the names from supertest
|
|
124
|
+
desiredHTTPResponse.text || desiredHTTPResponse.body || '',
|
|
125
|
+
desiredHTTPResponse.status || 200)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`un-mocked got() request: ${JSON.stringify(request)}`)
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// will respond to the next N requests with the specified N responses
|
|
133
|
+
gotMock.mockRespMulti = (...responses) => {
|
|
134
|
+
let idx = 0
|
|
135
|
+
function setupNextResponse () {
|
|
136
|
+
if (idx < responses.length) {
|
|
137
|
+
gotMock.mockResp(...responses[idx], setupNextResponse)
|
|
138
|
+
} else if (idx > responses.length) {
|
|
139
|
+
// exactly equal means we just got our last callback (okay)
|
|
140
|
+
throw new Error('more requests made than we had mock responses')
|
|
141
|
+
}
|
|
142
|
+
idx += 1
|
|
143
|
+
}
|
|
144
|
+
setupNextResponse()
|
|
145
|
+
}
|
|
146
|
+
return gotMock
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
BaseAppTest,
|
|
151
|
+
BaseTest,
|
|
152
|
+
mockGot,
|
|
153
|
+
runTests
|
|
154
|
+
}
|