add support for express route tests

Signed-off-by: Jakub Sokołowski <jakub@status.im>
This commit is contained in:
Jakub Sokołowski 2020-02-24 14:21:06 +01:00 committed by Jakub
parent 45d958e81a
commit b9001265e7
7 changed files with 193 additions and 219 deletions

View File

@ -38,18 +38,13 @@ docker-compose -p whatever -f docker-compose.yml build
It will build the image and start the container listening on port `8080`
## Running the tests
## Testing
To run the tests, first start the container, either in `production` or `development` mode.
```bash
yarn run tests
```
Then you can run `bash tests/run.sh -u localhost:8080` or to run against against the live server
`bash tests/run.sh -u http://join.status.im`
Uses the awesome `https://github.com/robwhitby/shakedown`
The pipeline is at:
`https://jenkins.status.im/job/misc/job/universal-links-handler/`
CI: https://jenkins.status.im/job/misc/job/universal-links-handler/
## Deployment

View File

@ -5,7 +5,8 @@
"scripts": {
"start": "node ./bin/www",
"watch": "nodemon ./bin/www",
"docker": "docker build -t statusteam/universal-links-handler:deploy ."
"docker": "docker build -t statusteam/universal-links-handler:deploy .",
"tests": "node -r esm tests/main.js"
},
"dependencies": {
"cookie-parser": "~1.4.3",
@ -19,6 +20,9 @@
"univeil": "^0.1.14"
},
"devDependencies": {
"nodemon": "^1.17.5"
"esm": "^3.2.25",
"nodemon": "^1.17.5",
"supertest": "^4.0.2",
"zora": "^3.1.8"
}
}

5
resources/links.json Normal file
View File

@ -0,0 +1,5 @@
{
"getStatus": "https://status.im/get/",
"playStore": "https://play.google.com/store/apps/details?id=im.status.ethereum",
"appleStore": "https://testflight.apple.com/join/J8EuJmey"
}

87
tests/main.js Normal file
View File

@ -0,0 +1,87 @@
import { test } from 'zora'
import request from 'supertest'
import app from '../app'
import links from '../resources/links.json'
import assetLinks from '../resources/assetlinks.json'
import appleSiteAssociation from '../resources/apple-app-site-association.json'
const host = 'join.status.im'
const chatKey = 'e139115a1acc72510388fcf7e1cf492784c9a839888b25271465f4f1baa38c2d3997f8fd78828eb8628bc3bb55ababd884c6002d18330d59c404cc9ce3e4fb35'
const chatName = 'Lavender Trivial Goral'
const srv = request(app)
const get = (path) => (
srv.get(path).set('Host', host)
)
test('test chat routes', t => {
t.test('/b/ens.domains', async t => {
const res = await get('/b/ens.domains')
t.equal(res.statusCode, 200, 'returns 200')
t.ok(res.text.includes(`href="http://${host}/b/ens.domains"`), 'contains link')
t.ok(res.text.includes('Browse to ens.domains in Status'), 'contains text')
})
t.test('/@jakubgs.eth', async t => {
const res = await get('/@jakubgs.eth')
t.equal(res.statusCode, 200, 'returns 200')
t.ok(res.text.includes(`href="http://${host}/@jakubgs.eth"`), 'contains link')
t.ok(res.text.includes('Chat and transact with <span>@jakubgs.eth</span> in Status.'), 'contains prompt')
})
t.test(`/0x04${chatKey.substr(0,8)}...`, async t => {
const res = await get(`/0x04${chatKey}`)
t.equal(res.statusCode, 200, 'returns 200')
t.ok(res.text.includes(`href="http://${host}/0x04${chatKey}"`), 'contains link')
t.ok(res.text.includes(`Chat and transact with <span>0x04${chatKey}</span> in Status.`), 'contains prompt')
t.ok(res.text.includes(chatName), 'contains chat name')
})
t.test('/status-test', async t => {
const res = await get('/status-test')
t.equal(res.statusCode, 200, 'returns 200')
t.ok(res.text.includes(`href="http://${host}/status-test"`), 'contains link')
t.ok(res.text.includes('Join public channel <span>#status-test</span> on Status.'), 'contains prompt')
})
})
test('test other routes', t => {
t.test('/health', async t => {
const res = await get('/health')
t.equal(res.statusCode, 200, 'returns 200')
t.equal(res.text, 'OK', 'returns OK')
})
t.test('/.well-known/assetlinks.json', async t => {
const res = await get('/.well-known/assetlinks.json')
t.equal(res.statusCode, 200, 'returns 200')
t.equal(res.text, JSON.stringify(assetLinks), 'returns asset links')
})
t.test('/.well-known/apple-app-site-association', async t => {
const res = await get('/.well-known/apple-app-site-association')
t.equal(res.statusCode, 200, 'returns 200')
t.equal(res.text, JSON.stringify(appleSiteAssociation), 'returns apple association')
})
})
test('catch-all route', t => {
t.test('redirects to status.im', async t => {
const res = await get('/')
t.equal(res.statusCode, 302, 'returns 302')
t.equal(res.headers.location, links.getStatus, 'sets location')
})
t.test('redirects to play store', async t => {
const res = await get('/').set('user-agent', 'xyz Android xyz')
t.equal(res.statusCode, 302, 'returns 302')
t.equal(res.headers.location, links.playStore, 'sets location')
})
t.test('redirects to apple store', async t => {
const res = await get('/').set('user-agent', 'xyz iPhone xyz')
t.equal(res.statusCode, 302, 'returns 302')
t.equal(res.headers.location, links.appleStore, 'sets location')
})
})

View File

@ -1,53 +0,0 @@
#!/bin/bash
source tests/shakedown.sh
until $(curl --output /dev/null --silent --head --fail $BASE_URL/health); do
printf '.'
sleep 1
done
shakedown GET /.well-known/assetlinks.json
status 200
content_type 'application/json'
contains 'sha256_cert_fingerprints'
shakedown GET /.well-known/apple-app-site-association
status 200
content_type 'application/json'
contains 'im.status.ethereum'
# Android test
shakedown GET / -H "User-Agent: Mozilla/5.0 (Linux; Android 7.0; SM-G892A Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Mobile Safari/537.36"
header_contains 'Location' 'https://play.google.com/store/apps/details?id=im.status.ethereum'
status 302
# IOS
shakedown GET / -H "User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"
header_contains 'Location' 'https://testflight.apple.com/join/J8EuJmey'
status 302
# Anything else
shakedown GET / -H "User-Agent: Unknown"
header_contains 'Location' 'https://status.im'
status 302
shakedown GET /chat/public/abc
status 200
shakedown GET /user/blah
status 200
shakedown GET /browse/www.test.com
status 200
shakedown GET /browse/www.test.com/blah/blah
status 200
shakedown GET /health
status 200
contains 'OK'
shakedown GET /extension/blah
status 200

View File

@ -1,149 +0,0 @@
#!/bin/bash
set -u
BASE_URL=${SHAKEDOWN_URL:-""}
CREDENTIALS=${SHAKEDOWN_CREDENTIALS:-""}
_usage() {
echo '
usage: $0 [options...]
Options:
-u <base URL> Base URL to test.
-c <user:password> Credentials for HTTP authentication.
'
exit 1
}
while getopts 'u:c:' OPTION
do
case $OPTION in
u) BASE_URL="$OPTARG";;
c) CREDENTIALS="$OPTARG";;
*) _usage;;
esac
done
echo "Starting shakedown of ${BASE_URL:-"[base URL not set]"}"
STATE=""
FAIL_COUNT=0
PASS_COUNT=0
WORKING_DIR=$(mktemp -d -t shakedown.XXXXXX)
RESPONSE_BODY="${WORKING_DIR}/body"
RESPONSE_HEADERS="${WORKING_DIR}/headers"
AUTH=""
if [ -n "${CREDENTIALS}" ]; then
AUTH="--anyauth --user ${CREDENTIALS}"
fi
CURL="curl -sS ${AUTH} -D ${RESPONSE_HEADERS} --connect-timeout 5 --max-time 30"
CRED=$(tput setaf 1 2> /dev/null)
CGREEN=$(tput setaf 2 2> /dev/null)
CDEFAULT=$(tput sgr0 2> /dev/null)
_pass() {
echo " ${CGREEN}${1}${CDEFAULT}"
}
_fail() {
STATE="fail"
echo " ${CRED}${1}${CDEFAULT}"
}
_start_test() {
_finish_test
STATE="pass"
}
_finish_test() {
if [ "$STATE" = "pass" ]; then
((PASS_COUNT++))
elif [ "$STATE" = "fail" ]; then
((FAIL_COUNT++))
fi
}
_finish() {
_finish_test
rm -rf "${WORKING_DIR}"
echo
MSG="Shakedown complete. ${PASS_COUNT} passed, ${FAIL_COUNT} failed."
[[ ${FAIL_COUNT} -eq 0 ]] && echo "${CGREEN}${MSG}${CDEFAULT}" || echo "${CRED}${MSG} You're busted.${CDEFAULT}"
exit ${FAIL_COUNT}
}
trap _finish EXIT
# start test
# $1 METHOD
# $2 URL
# $3..$n Custom CURL options
shakedown() {
_start_test
METHOD="$1"
URL="$2"
if ! [[ $URL == http* ]]; then
URL="${BASE_URL}${URL}"
fi
echo
echo "${METHOD} ${URL}"
METHOD_OPT="-X ${METHOD}"
if [ "${METHOD}" = "HEAD" ]; then
METHOD_OPT="-I"
fi
${CURL} ${METHOD_OPT} "${@:3}" "${URL}" > ${RESPONSE_BODY}
}
# assertions
header() {
grep -Fq "${1}" "${RESPONSE_HEADERS}" && _pass "header ${1}" || _fail "header ${1}"
}
no_header() {
grep -Fq "${1}" "${RESPONSE_HEADERS}" && _fail "no_header ${1}" || _pass "no_header ${1}"
}
status() {
STATUS_CODE=$(grep -Eo "^HTTP.+ [1-5][0-9][0-9] " ${RESPONSE_HEADERS} | grep -Eo '[1-5][0-9][0-9]' | tail -n1)
[[ "${STATUS_CODE}" = "${1}" ]] && _pass "status ${1}" || _fail "status ${1} (actual: ${STATUS_CODE})"
}
contains() {
MSG="contains \"${1}\""
grep -Fq "${1}" "${RESPONSE_BODY}" && _pass "${MSG}" || _fail "${MSG}"
}
matches() {
MSG="matches \"${1}\""
grep -Eq "${1}" "${RESPONSE_BODY}" && _pass "${MSG}" || _fail "${MSG}"
}
content_type() {
CT_HEADER="$(_get_header 'Content-Type')"
echo "${CT_HEADER}" | grep -Fq "${1}" && _pass "Content-Type: ${1}" || _fail "Content-Type: ${1} (actual: ${CT_HEADER})"
}
header_contains() {
HEADER_NAME=${1}
HEADER="$(_get_header $HEADER_NAME)"
echo "${HEADER}" | grep -Fq "${2}" && _pass "${HEADER_NAME}: ${2}" || _fail "${HEADER_NAME}: ${2} (actual: ${HEADER})"
}
_get_header() {
grep -i -F "${1}" "${RESPONSE_HEADERS}" | tr -d '\r'
}
# debug
print_headers() {
cat "${RESPONSE_HEADERS}"
}
print_body() {
cat "${RESPONSE_BODY}"
}

View File

@ -82,6 +82,11 @@ async-each@^1.0.1:
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
atob@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
@ -318,7 +323,14 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
component-emitter@^1.2.1:
combined-stream@^1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
component-emitter@^1.2.0, component-emitter@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
@ -368,6 +380,11 @@ cookie@0.3.1:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
cookiejar@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==
copy-descriptor@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
@ -406,7 +423,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@~2.6.9:
dependencies:
ms "2.0.0"
debug@^3.2.6:
debug@^3.1.0, debug@^3.2.6:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
@ -450,6 +467,11 @@ define-property@^2.0.2:
is-descriptor "^1.0.2"
isobject "^3.0.1"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
@ -507,6 +529,11 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
esm@^3.2.25:
version "3.2.25"
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
@ -589,6 +616,11 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
assign-symbols "^1.0.0"
is-extendable "^1.0.1"
extend@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
extglob@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
@ -643,6 +675,20 @@ for-in@^1.0.2:
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
form-data@^2.3.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.6"
mime-types "^2.1.12"
formidable@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659"
integrity sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -1097,7 +1143,7 @@ merge-descriptors@1.0.1:
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
methods@~1.1.2:
methods@^1.1.1, methods@^1.1.2, methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
@ -1126,7 +1172,7 @@ mime-db@1.43.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
mime-types@~2.1.24:
mime-types@^2.1.12, mime-types@~2.1.24:
version "2.1.26"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
@ -1138,6 +1184,11 @@ mime@1.4.1:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
mime@^1.4.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@ -1407,6 +1458,11 @@ qs@6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
qs@^6.5.1:
version "6.9.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9"
integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA==
range-parser@~1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@ -1432,7 +1488,7 @@ rc@^1.0.1, rc@^1.1.6:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
readable-stream@^2.0.2:
readable-stream@^2.0.2, readable-stream@^2.3.5:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@ -1736,6 +1792,30 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
superagent@^3.8.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128"
integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==
dependencies:
component-emitter "^1.2.0"
cookiejar "^2.1.0"
debug "^3.1.0"
extend "^3.0.0"
form-data "^2.3.1"
formidable "^1.2.0"
methods "^1.1.1"
mime "^1.4.1"
qs "^6.5.1"
readable-stream "^2.3.5"
supertest@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/supertest/-/supertest-4.0.2.tgz#c2234dbdd6dc79b6f15b99c8d6577b90e4ce3f36"
integrity sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==
dependencies:
methods "^1.1.2"
superagent "^3.8.3"
supports-color@^5.3.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@ -1972,3 +2052,8 @@ yargs@^13.2.4:
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^13.1.1"
zora@^3.1.8:
version "3.1.8"
resolved "https://registry.yarnpkg.com/zora/-/zora-3.1.8.tgz#84cf75a057005931058db43366cad8c75a21f04c"
integrity sha512-AArEyKiLWi3eLXW2uRbfPvANfSQgV8VHoCuXCihCTQyUv7brFrghGbsUqKxqucc+QodQ1G2+O8Gpsz8RVpeiRQ==