From fe1b0f7a291f7d4351e8885d00cc484bb19d1afc Mon Sep 17 00:00:00 2001 From: BlankParticle Date: Mon, 20 Jul 2026 18:13:39 +0530 Subject: [PATCH 1/2] test: replace supertest --- package.json | 2 - pnpm-lock.yaml | 372 ---------- src/request.ts | 6 +- .../static-with-precompressed/hello.bin.br | 2 +- .../static-with-precompressed/hello.txt.br | 2 +- test/helpers/request.ts | 171 ++++- test/listener.test.ts | 183 +++-- test/serve-static.test.ts | 428 +++++++----- test/server.test.ts | 643 ++++++++++-------- test/utils/response.test.ts | 8 +- tsconfig.json | 4 +- 11 files changed, 873 insertions(+), 948 deletions(-) diff --git a/package.json b/package.json index 293dec47..43fd1633 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,6 @@ "devDependencies": { "@hono/eslint-config": "^1.0.1", "@types/node": "^20.10.0", - "@types/supertest": "^2.0.12", "@types/ws": "^8.18.1", "@whatwg-node/fetch": "^0.9.14", "eslint": "^9.10.0", @@ -104,7 +103,6 @@ "np": "^11.2.1", "prettier": "^3.2.4", "publint": "^0.3.18", - "supertest": "^7.2.2", "tsdown": "^0.20.3", "typescript": "^5.3.2", "vitest": "^4.0.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b214ddc..b8a0ae70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,6 @@ importers: '@types/node': specifier: ^20.10.0 version: 20.19.37 - '@types/supertest': - specifier: ^2.0.12 - version: 2.0.16 '@types/ws': specifier: ^8.18.1 version: 8.18.1 @@ -38,9 +35,6 @@ importers: publint: specifier: ^0.3.18 version: 0.3.18 - supertest: - specifier: ^7.2.2 - version: 7.2.2 tsdown: specifier: ^0.20.3 version: 0.20.3(publint@0.3.18)(typescript@5.9.3) @@ -331,10 +325,6 @@ packages: '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} - '@noble/hashes@1.8.0': - resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} - engines: {node: ^14.21.3 || >=16} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -359,9 +349,6 @@ packages: '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} - '@paralleldrive/cuid2@2.3.1': - resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} - '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -693,9 +680,6 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/cookiejar@2.1.5': - resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} - '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -708,21 +692,12 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/methods@1.1.4': - resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} - '@types/node@20.19.37': resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/superagent@8.1.9': - resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} - - '@types/supertest@2.0.16': - resolution: {integrity: sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==} - '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -1008,9 +983,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1019,9 +991,6 @@ packages: resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==} engines: {node: '>=20.19.0'} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - atomically@2.1.1: resolution: {integrity: sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==} @@ -1062,14 +1031,6 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1161,17 +1122,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - comment-parser@1.4.5: resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==} engines: {node: '>= 12.0.0'} - component-emitter@1.3.1: - resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1185,13 +1139,6 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookiejar@2.1.4: - resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - cosmiconfig@9.0.2: resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} engines: {node: '>=14'} @@ -1247,17 +1194,10 @@ packages: resolution: {integrity: sha512-gPqh0mKTPvaUZGAuHbrBUYKZWBNAeHG7TU3QH5EhVwPMyKvmfJaNXhcD2jTcXsJRRcffuho4vaYweu80dRrMGA==} engines: {node: '>=18'} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dezalgo@1.0.4: - resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} - dot-prop@9.0.0: resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} engines: {node: '>=18'} @@ -1271,10 +1211,6 @@ packages: oxc-resolver: optional: true - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - elegant-spinner@1.0.1: resolution: {integrity: sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==} engines: {node: '>=0.10.0'} @@ -1304,25 +1240,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - escape-goat@4.0.0: resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} engines: {node: '>=12'} @@ -1486,9 +1406,6 @@ packages: fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} - fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-string-truncated-width@3.0.3: resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} @@ -1553,34 +1470,15 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - - formidable@3.5.4: - resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} - engines: {node: '>=14.0.0'} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -1622,10 +1520,6 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} @@ -1648,18 +1542,6 @@ packages: resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} engines: {node: '>=12'} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - hono@4.12.8: resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} engines: {node: '>=16.9.0'} @@ -2052,10 +1934,6 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - meow@14.1.0: resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} engines: {node: '>=20'} @@ -2067,27 +1945,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} - hasBin: true - mimic-fn@1.2.0: resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} engines: {node: '>=4'} @@ -2173,16 +2034,9 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@2.0.1: resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} engines: {node: '>=4'} @@ -2353,10 +2207,6 @@ packages: resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} engines: {node: '>=12.20'} - qs@6.15.0: - resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} - engines: {node: '>=0.6'} - quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} @@ -2494,22 +2344,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2620,14 +2454,6 @@ packages: resolution: {integrity: sha512-BWnYJElmHbYZ/zKevy+TG+SsyoFCmRPDHJbR1MzLxkPOv1Jp/4hGhVUtP98s+wZBsBsHwCXvPTP0x287/WMjGg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - superagent@10.3.0: - resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} - engines: {node: '>=14.18.0'} - - supertest@7.2.2: - resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} - engines: {node: '>=14.18.0'} - supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -2935,9 +2761,6 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -3245,8 +3068,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@noble/hashes@1.8.0': {} - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3267,10 +3088,6 @@ snapshots: '@package-json/types@0.0.12': {} - '@paralleldrive/cuid2@2.3.1': - dependencies: - '@noble/hashes': 1.8.0 - '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -3456,8 +3273,6 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/cookiejar@2.1.5': {} - '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -3466,25 +3281,12 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/methods@1.1.4': {} - '@types/node@20.19.37': dependencies: undici-types: 6.21.0 '@types/normalize-package-data@2.4.4': {} - '@types/superagent@8.1.9': - dependencies: - '@types/cookiejar': 2.1.5 - '@types/methods': 1.1.4 - '@types/node': 20.19.37 - form-data: 4.0.5 - - '@types/supertest@2.0.16': - dependencies: - '@types/superagent': 8.1.9 - '@types/ws@8.18.1': dependencies: '@types/node': 20.19.37 @@ -3749,8 +3551,6 @@ snapshots: argparse@2.0.1: {} - asap@2.0.6: {} - assertion-error@2.0.1: {} ast-kit@3.0.0-beta.1: @@ -3759,8 +3559,6 @@ snapshots: estree-walker: 3.0.3 pathe: 2.0.3 - asynckit@0.4.0: {} - atomically@2.1.1: dependencies: stubborn-fs: 2.0.0 @@ -3806,16 +3604,6 @@ snapshots: cac@6.7.14: {} - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - callsites@3.1.0: {} camelcase@8.0.0: {} @@ -3899,14 +3687,8 @@ snapshots: color-name@1.1.4: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - comment-parser@1.4.5: {} - component-emitter@1.3.1: {} - concat-map@0.0.1: {} config-chain@1.1.13: @@ -3923,10 +3705,6 @@ snapshots: convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} - - cookiejar@2.1.4: {} - cosmiconfig@9.0.2(typescript@5.9.3): dependencies: env-paths: 2.2.1 @@ -3977,27 +3755,14 @@ snapshots: presentable-error: 0.0.1 slash: 5.1.0 - delayed-stream@1.0.0: {} - detect-libc@2.1.2: {} - dezalgo@1.0.4: - dependencies: - asap: 2.0.6 - wrappy: 1.0.2 - dot-prop@9.0.0: dependencies: type-fest: 4.41.0 dts-resolver@2.1.3: {} - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - elegant-spinner@1.0.1: {} emoji-regex@10.6.0: {} @@ -4019,23 +3784,8 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - es-module-lexer@2.0.0: {} - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - escape-goat@4.0.0: {} escape-string-regexp@1.0.5: {} @@ -4244,8 +3994,6 @@ snapshots: dependencies: fast-decode-uri-component: 1.0.1 - fast-safe-stringify@2.1.1: {} - fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: @@ -4308,45 +4056,11 @@ snapshots: flatted@3.4.2: {} - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - - formidable@3.5.4: - dependencies: - '@paralleldrive/cuid2': 2.3.1 - dezalgo: 1.0.4 - once: 1.4.0 - fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - get-east-asian-width@1.6.0: {} - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - get-stream@6.0.1: {} get-stream@9.0.1: @@ -4387,8 +4101,6 @@ snapshots: globrex@0.1.2: {} - gopd@1.2.0: {} - graceful-fs@4.2.10: {} graceful-fs@4.2.11: {} @@ -4403,16 +4115,6 @@ snapshots: has-flag@5.0.1: {} - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - hono@4.12.8: {} hookable@6.1.0: {} @@ -4758,29 +4460,17 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - math-intrinsics@1.1.0: {} - meow@14.1.0: {} merge-stream@2.0.0: {} merge2@1.4.1: {} - methods@1.1.2: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mime@2.6.0: {} - mimic-fn@1.2.0: {} mimic-fn@2.1.0: {} @@ -4892,14 +4582,8 @@ snapshots: object-assign@4.1.1: {} - object-inspect@1.13.4: {} - obug@2.1.1: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - onetime@2.0.1: dependencies: mimic-fn: 1.2.0 @@ -5050,10 +4734,6 @@ snapshots: dependencies: escape-goat: 4.0.0 - qs@6.15.0: - dependencies: - side-channel: 1.1.0 - quansync@1.0.0: {} queue-microtask@1.2.3: {} @@ -5228,34 +4908,6 @@ snapshots: shebang-regex@3.0.0: {} - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -5352,28 +5004,6 @@ snapshots: escape-string-regexp: 5.0.0 unique-string: 3.0.0 - superagent@10.3.0: - dependencies: - component-emitter: 1.3.1 - cookiejar: 2.1.4 - debug: 4.4.3 - fast-safe-stringify: 2.1.1 - form-data: 4.0.5 - formidable: 3.5.4 - methods: 1.1.2 - mime: 2.6.0 - qs: 6.15.0 - transitivePeerDependencies: - - supports-color - - supertest@7.2.2: - dependencies: - cookie-signature: 1.2.2 - methods: 1.1.2 - superagent: 10.3.0 - transitivePeerDependencies: - - supports-color - supports-color@10.2.2: {} supports-color@2.0.0: {} @@ -5634,8 +5264,6 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 - wrappy@1.0.2: {} - ws@8.19.0: {} wsl-utils@0.3.1: diff --git a/src/request.ts b/src/request.ts index a6a392aa..0a907c22 100644 --- a/src/request.ts +++ b/src/request.ts @@ -43,7 +43,7 @@ export class Request extends GlobalRequest { } } -export const newHeadersFromIncoming = (incoming: IncomingMessage | Http2ServerRequest) => { +export const newHeadersFromIncoming = (incoming: Pick) => { const headerRecord: [string, string][] = [] const rawHeaders = incoming.rawHeaders for (let i = 0, len = rawHeaders.length; i < len; i += 2) { @@ -715,7 +715,9 @@ Object.defineProperty(requestPrototype, 'blob', { return readBodyWithFastPath(this, 'blob', (buf, request) => { const type = contentType(request) const init = type ? { headers: { 'content-type': type } } : undefined - return new Response(buf, init).blob() + // Buffer is typed over ArrayBufferLike, but request bodies are always + // backed by an ArrayBuffer, so cast instead of copying the bytes + return new Response(buf as Uint8Array, init).blob() }) }, }) diff --git a/test/assets/static-with-precompressed/hello.bin.br b/test/assets/static-with-precompressed/hello.bin.br index dba78128..2584ce17 100644 --- a/test/assets/static-with-precompressed/hello.bin.br +++ b/test/assets/static-with-precompressed/hello.bin.br @@ -1 +1 @@ - €Hello br Compressed \ No newline at end of file +Hello br Compressed \ No newline at end of file diff --git a/test/assets/static-with-precompressed/hello.txt.br b/test/assets/static-with-precompressed/hello.txt.br index dba78128..2584ce17 100644 --- a/test/assets/static-with-precompressed/hello.txt.br +++ b/test/assets/static-with-precompressed/hello.txt.br @@ -1 +1 @@ - €Hello br Compressed \ No newline at end of file +Hello br Compressed \ No newline at end of file diff --git a/test/helpers/request.ts b/test/helpers/request.ts index 10ec0a88..7a342429 100644 --- a/test/helpers/request.ts +++ b/test/helpers/request.ts @@ -1,8 +1,10 @@ import { once } from 'node:events' import { request as requestHTTP } from 'node:http' import type { IncomingMessage, RequestOptions } from 'node:http' +import { connect } from 'node:http2' +import type { IncomingHttpHeaders as IncomingHttp2Headers } from 'node:http2' +import { Server as HttpsServer, request as requestHTTPS } from 'node:https' import type { AddressInfo } from 'node:net' -import { Readable } from 'node:stream' import { newHeadersFromIncoming } from '../../src/request' import { GlobalResponse } from '../../src/response' import type { ServerType } from '../../src/types' @@ -15,39 +17,174 @@ export type ServerRequestInit = Omit< path: string } -export const requestServer = async ( - server: ServerType, - init: ServerRequestInit -): Promise => { - const address = server.address() as AddressInfo | null +export type ServerRequestHooks = { + onHeaders?: (headers: Headers, status: number) => void + onChunk?: (chunk: Buffer, index: number) => void +} + +export type Http2RequestInit = { + headers?: Record + method?: string + path: string +} + +const ensureListening = async (server: ServerType) => { + let address = server.address() as AddressInfo | null + let startedByHelper = false + if (!address) { + server.listen(0, '127.0.0.1') + await once(server, 'listening') + startedByHelper = true + address = server.address() as AddressInfo | null + } if (!address) { throw new Error('Server is not listening on a TCP address') } + return { + address, + // a no-op if the server was already listening before the request, its + // owner is responsible for closing it in that case + close: async () => { + if (startedByHelper) { + await new Promise((resolve) => server.close(() => resolve())) + } + }, + } +} + +const sendRequest = (server: ServerType, address: AddressInfo, init: ServerRequestInit) => { const { body, path, ...options } = init - const request = requestHTTP({ + const common = { ...options, agent: false, hostname: address.address, path, port: address.port, - }) + } + const request = + server instanceof HttpsServer + ? requestHTTPS({ ...common, rejectUnauthorized: false }) + : requestHTTP(common) request.end(body) + return request +} +const receiveResponse = async (request: ReturnType) => { const [incoming] = (await once(request, 'response')) as [IncomingMessage] const status = incoming.statusCode if (!status) { throw new Error('Server response did not include a status code') } + return { incoming, status } +} - const responseBody = - options.method?.toUpperCase() === 'HEAD' || [101, 204, 205, 304].includes(status) - ? null - : (Readable.toWeb(incoming) as ReadableStream) +export const requestServer = async ( + server: ServerType, + init: ServerRequestInit +): Promise => { + const { address, close } = await ensureListening(server) - return new GlobalResponse(responseBody, { - headers: newHeadersFromIncoming(incoming), - status, - statusText: incoming.statusMessage, - }) + // the request may fail mid-flight (e.g. aborted via `init.signal`), so the + // helper-started server is always fully closed before returning or + // surfacing an error + try { + const { incoming, status } = await receiveResponse(sendRequest(server, address, init)) + + const hasBody = init.method?.toUpperCase() !== 'HEAD' && ![101, 204, 205, 304].includes(status) + + let responseBody: BodyInit | null = null + if (hasBody) { + // buffer the body so a helper-started server can be fully closed before + // returning, otherwise a subsequent request could hit a half-closed server + const chunks: Buffer[] = [] + for await (const chunk of incoming) { + chunks.push(chunk) + } + responseBody = new Uint8Array(Buffer.concat(chunks)) + } + + return new GlobalResponse(responseBody, { + headers: newHeadersFromIncoming(incoming), + status, + statusText: incoming.statusMessage, + }) + } finally { + await close() + } +} + +/** + * Same as `requestServer`, but exposes the raw body chunks for tests that + * assert streaming behavior: `onHeaders` fires before any body data arrives + * and `onChunk` fires per received chunk, so a test can synchronize with the + * server mid-response. Chunk boundaries are preserved by the chunked + * transfer-encoding framing. + */ +export const requestServerChunked = async ( + server: ServerType, + init: ServerRequestInit, + hooks: ServerRequestHooks = {} +): Promise<{ chunks: Buffer[]; response: Response }> => { + const { address, close } = await ensureListening(server) + + try { + const { incoming, status } = await receiveResponse(sendRequest(server, address, init)) + const headers = newHeadersFromIncoming(incoming) + hooks.onHeaders?.(headers, status) + + const chunks: Buffer[] = [] + incoming.on('data', (chunk: Buffer) => { + hooks.onChunk?.(chunk, chunks.length) + chunks.push(chunk) + }) + await once(incoming, 'end') + + return { + chunks, + response: new GlobalResponse(new Uint8Array(Buffer.concat(chunks)), { + headers, + status, + statusText: incoming.statusMessage, + }), + } + } finally { + await close() + } +} + +export const requestServerHttp2 = async ( + server: ServerType, + init: Http2RequestInit +): Promise => { + const { address, close } = await ensureListening(server) + const client = connect(`http://${address.address}:${address.port}`) + // stream errors reject `once()` below, a session error would otherwise crash + client.once('error', () => {}) + + try { + const stream = client.request({ + ':method': init.method ?? 'GET', + ':path': init.path, + ...init.headers, + }) + stream.end() + + const [incomingHeaders, , rawHeaders] = (await once(stream, 'response')) as [ + IncomingHttp2Headers, + number, + string[], + ] + const status = Number(incomingHeaders[':status']) + const headers = newHeadersFromIncoming({ rawHeaders }) + + const chunks: Buffer[] = [] + stream.on('data', (chunk: Buffer) => chunks.push(chunk)) + await once(stream, 'end') + + return new GlobalResponse(new Uint8Array(Buffer.concat(chunks)), { headers, status }) + } finally { + client.close() + await close() + } } diff --git a/test/listener.test.ts b/test/listener.test.ts index 505db60d..6df58dd1 100644 --- a/test/listener.test.ts +++ b/test/listener.test.ts @@ -1,8 +1,8 @@ -import request from 'supertest' import { createServer } from 'node:http' import { getRequestListener } from '../src/listener' import { GlobalRequest, Request as LightweightRequest, RequestError } from '../src/request' import { GlobalResponse, Response as LightweightResponse } from '../src/response' +import { requestServer } from './helpers/request' const withTimeout = async (promise: Promise, message: string): Promise => { let timeoutId: ReturnType | undefined @@ -27,7 +27,7 @@ const runRequestAndCollectOutgoingEvents = async ( fetchCallback: Parameters[0] ): Promise<{ closeListenerCount: number - response: request.Response + response: Response }> => { let closeListenerCount = 0 const requestListener = getRequestListener(fetchCallback) @@ -44,12 +44,8 @@ const runRequestAndCollectOutgoingEvents = async ( await requestListener(req, res) }) - try { - const response = await request(server).get('/') - return { closeListenerCount, response } - } finally { - server.close() - } + const response = await requestServer(server, { method: 'GET', path: '/' }) + return { closeListenerCount, response } } describe('Invalid request', () => { @@ -58,12 +54,21 @@ describe('Invalid request', () => { const server = createServer(requestListener) it('Should return server error for a request w/o host header', async () => { - const res = await request(server).get('/').set('Host', '').send() + const res = await requestServer(server, { + method: 'GET', + path: '/', + headers: { host: '' }, + setHost: false, + }) expect(res.status).toBe(400) }) it('Should return server error for a request invalid host header', async () => { - const res = await request(server).get('/').set('Host', 'a b').send() + const res = await requestServer(server, { + method: 'GET', + path: '/', + headers: { host: 'a b' }, + }) expect(res.status).toBe(400) }) }) @@ -81,17 +86,30 @@ describe('Invalid request', () => { const server = createServer(requestListener) it('Should return server error for a request w/o host header', async () => { - const res = await request(server).get('/').set('Host', '').send() + const res = await requestServer(server, { + method: 'GET', + path: '/', + headers: { host: '' }, + setHost: false, + }) expect(res.status).toBe(400) }) it('Should return server error for a request invalid host header', async () => { - const res = await request(server).get('/').set('Host', 'a b').send() + const res = await requestServer(server, { + method: 'GET', + path: '/', + headers: { host: 'a b' }, + }) expect(res.status).toBe(400) }) it('Should return server error for host header with path', async () => { - const res = await request(server).get('/').set('Host', 'a/b').send() + const res = await requestServer(server, { + method: 'GET', + path: '/', + headers: { host: 'a/b' }, + }) expect(res.status).toBe(400) }) }) @@ -103,12 +121,21 @@ describe('Invalid request', () => { const server = createServer(requestListener) it('Should return 200 for a request w/o host header', async () => { - const res = await request(server).get('/').set('Host', '').send() + const res = await requestServer(server, { + method: 'GET', + path: '/', + headers: { host: '' }, + setHost: false, + }) expect(res.status).toBe(200) }) it('Should return server error for a request invalid host header', async () => { - const res = await request(server).get('/').set('Host', 'a b').send() + const res = await requestServer(server, { + method: 'GET', + path: '/', + headers: { host: 'a b' }, + }) expect(res.status).toBe(400) }) }) @@ -123,7 +150,7 @@ describe('Invalid request', () => { const server = createServer(requestListener) it('Should return a 500 for a malformed response', async () => { - const res = await request(server).get('/').send() + const res = await requestServer(server, { method: 'GET', path: '/' }) expect(res.status).toBe(500) }) }) @@ -136,10 +163,10 @@ describe('Response headers', () => { const server = createServer(requestListener) it('Should not set content-type for a null body response', async () => { - const res = await request(server).get('/').send() + const res = await requestServer(server, { method: 'GET', path: '/' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBeUndefined() - expect(res.text).toBe('') + expect(res.headers.get('content-type')).toBeNull() + expect(await res.text()).toBe('') }) }) @@ -169,10 +196,10 @@ describe('Error handling - sync fetchCallback', () => { return new Response(`${err}`, { status: 500, headers: { 'my-custom-header': 'hi' } }) }) - const res = await request(server).get('/throw-error') + const res = await requestServer(server, { method: 'GET', path: '/throw-error' }) expect(res.status).toBe(500) - expect(res.headers['my-custom-header']).toBe('hi') - expect(res.text).toBe('Error: thrown error') + expect(res.headers.get('my-custom-header')).toBe('hi') + expect(await res.text()).toBe('Error: thrown error') }) it('Should not set the response if the error handler does not return a response', async () => { @@ -180,10 +207,10 @@ describe('Error handling - sync fetchCallback', () => { // do something else, such as passing error to vite next middleware, etc }) - const res = await request(server).get('/throw-error') + const res = await requestServer(server, { method: 'GET', path: '/throw-error' }) expect(errorHandler).toHaveBeenCalledTimes(1) expect(res.status).toBe(500) - expect(res.text).toBe('error handler did not return a response') + expect(await res.text()).toBe('error handler did not return a response') }) }) @@ -213,10 +240,10 @@ describe('Error handling - async fetchCallback', () => { return new Response(`${err}`, { status: 500, headers: { 'my-custom-header': 'hi' } }) }) - const res = await request(server).get('/throw-error') + const res = await requestServer(server, { method: 'GET', path: '/throw-error' }) expect(res.status).toBe(500) - expect(res.headers['my-custom-header']).toBe('hi') - expect(res.text).toBe('Error: thrown error') + expect(res.headers.get('my-custom-header')).toBe('hi') + expect(await res.text()).toBe('Error: thrown error') }) it('Should not set the response if the error handler does not return a response', async () => { @@ -224,10 +251,10 @@ describe('Error handling - async fetchCallback', () => { // do something else, such as passing error to vite next middleware, etc }) - const res = await request(server).get('/throw-error') + const res = await requestServer(server, { method: 'GET', path: '/throw-error' }) expect(errorHandler).toHaveBeenCalledTimes(1) expect(res.status).toBe(500) - expect(res.text).toBe('error handler did not return a response') + expect(await res.text()).toBe('error handler did not return a response') }) }) @@ -253,10 +280,6 @@ describe('Abort request', () => { }) }) - afterAll(() => { - server.close() - }) - it.each(['get', 'put', 'patch', 'delete'] as const)( 'should emit an abort event when the nodejs %s request is aborted', async (method) => { @@ -268,15 +291,19 @@ describe('Abort request', () => { } }) - const req = request(server) - [method]('/abort') - .end(() => {}) + const controller = new AbortController() + const resPromise = requestServer(server, { + method: method.toUpperCase(), + path: '/abort', + signal: controller.signal, + }).catch(() => {}) await reqReadyPromise - req.abort() + controller.abort() await abortedPromise + await resPromise expect(requests).toHaveLength(1) const abortedReq = requests[0] @@ -302,15 +329,19 @@ describe('Abort request', () => { reqReadyResolve = r }) - const req = request(server) - [method]('/abort') - .end(() => {}) + const controller = new AbortController() + const resPromise = requestServer(server, { + method: method.toUpperCase(), + path: '/abort', + signal: controller.signal, + }).catch(() => {}) await reqReadyPromise - req.abort() + controller.abort() await abortedPromise + await resPromise } expect(requests).toHaveLength(1) @@ -332,15 +363,19 @@ describe('Abort request', () => { reqReadyResolve = r }) - const req = request(server) - [method]('/abort') - .end(() => {}) + const controller = new AbortController() + const resPromise = requestServer(server, { + method: method.toUpperCase(), + path: '/abort', + signal: controller.signal, + }).catch(() => {}) await reqReadyPromise - req.abort() + controller.abort() await abortedPromise + await resPromise } expect(requests).toHaveLength(2) @@ -359,8 +394,12 @@ describe('Abort request', () => { } const requestListener = getRequestListener(fetchCallback) const server = createServer(requestListener) - const req = request(server).post('/abort').timeout({ deadline: 1 }) - await expect(req).rejects.toHaveProperty('timeout') + const req = requestServer(server, { + method: 'POST', + path: '/abort', + signal: AbortSignal.timeout(1), + }) + await expect(req).rejects.toThrow() }) }) @@ -401,17 +440,17 @@ describe('Abort request - error path', () => { const requestListener = getRequestListener(fetchCallback, { errorHandler }) const server = createServer(requestListener) - try { - const req = request(server) - .get('/') - .end(() => {}) - await withTimeout(errorHandlerStarted, 'error handler did not start') - req.abort() - await withTimeout(abortedPromise, 'request abort did not propagate') - expect(capturedReq?.signal.aborted).toBe(true) - } finally { - server.close() - } + const controller = new AbortController() + const resPromise = requestServer(server, { + method: 'GET', + path: '/', + signal: controller.signal, + }).catch(() => {}) + await withTimeout(errorHandlerStarted, 'error handler did not start') + controller.abort() + await withTimeout(abortedPromise, 'request abort did not propagate') + expect(capturedReq?.signal.aborted).toBe(true) + await resPromise } it.each(['sync', 'async'] as const)( @@ -437,9 +476,9 @@ describe('Abort request - cacheable response path', () => { expect(closeListenerCount).toBe(0) if (response.status === 204) { - expect(response.text).toBe('') + expect(response.body).toBeNull() } else { - expect(response.text).toBe('fast path') + expect(await response.text()).toBe('fast path') } } ) @@ -455,7 +494,7 @@ describe('Abort request - cacheable response path', () => { ) expect(closeListenerCount).toBe(1) - expect(response.text).toBe('blob-body') + expect(await response.text()).toBe('blob-body') }) it('should abort request signal when client disconnects during sync cacheable ReadableStream response', async () => { @@ -489,17 +528,17 @@ describe('Abort request - cacheable response path', () => { const requestListener = getRequestListener(fetchCallback) const server = createServer(requestListener) - try { - const req = request(server) - .get('/') - .end(() => {}) - await withTimeout(streamConstructed, 'stream body was not constructed') - req.abort() - await withTimeout(abortedPromise, 'request abort did not propagate for cacheable stream') - expect(capturedReq?.signal.aborted).toBe(true) - } finally { - server.close() - } + const controller = new AbortController() + const resPromise = requestServer(server, { + method: 'GET', + path: '/', + signal: controller.signal, + }).catch(() => {}) + await withTimeout(streamConstructed, 'stream body was not constructed') + controller.abort() + await withTimeout(abortedPromise, 'request abort did not propagate for cacheable stream') + expect(capturedReq?.signal.aborted).toBe(true) + await resPromise }) }) diff --git a/test/serve-static.test.ts b/test/serve-static.test.ts index f0e2417a..ef6cc340 100644 --- a/test/serve-static.test.ts +++ b/test/serve-static.test.ts @@ -1,5 +1,4 @@ import { Hono } from 'hono' -import request from 'supertest' import { chmodSync, rmSync, statSync, symlinkSync } from 'node:fs' import path from 'node:path' import { serveStatic } from './../src/serve-static' @@ -68,210 +67,263 @@ describe('Serve Static Middleware', () => { const server = createAdaptorServer(app) - beforeAll( - () => - new Promise((resolve, reject) => { - server.once('error', reject) - server.listen(0, '127.0.0.1', resolve) - }) - ) - - afterAll( - () => - new Promise((resolve, reject) => { - server.close((error) => (error ? reject(error) : resolve())) - }) - ) - it('Should return index.html', async () => { - const res = await request(server).get('/static/') + const res = await requestServer(server, { method: 'GET', path: '/static/' }) expect(res.status).toBe(200) - expect(res.text).toBe('

Hello Hono

') - expect(res.headers['content-type']).toBe('text/html; charset=utf-8') - expect(res.headers['x-custom']).toMatch( + expect(await res.text()).toBe('

Hello Hono

') + expect(res.headers.get('content-type')).toBe('text/html; charset=utf-8') + expect(res.headers.get('x-custom')).toMatch( /Found the file at test[\/\\]assets[\/\\]static[\/\\]index\.html$/ ) }) it('Should return hono.html', async () => { - const res = await request(server).get('/static/hono.html') + const res = await requestServer(server, { method: 'GET', path: '/static/hono.html' }) expect(res.status).toBe(200) - expect(res.text).toBe('

This is Hono.html

') - expect(res.headers['content-type']).toBe('text/html; charset=utf-8') + expect(await res.text()).toBe('

This is Hono.html

') + expect(res.headers.get('content-type')).toBe('text/html; charset=utf-8') }) it('Should return correct headers for icons', async () => { - const res = await request(server).get('/favicon.ico') + const res = await requestServer(server, { method: 'GET', path: '/favicon.ico' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('image/x-icon') + expect(res.headers.get('content-type')).toBe('image/x-icon') }) it('Should return correct headers and data for json files', async () => { - const res = await request(server).get('/static/data.json') + const res = await requestServer(server, { method: 'GET', path: '/static/data.json' }) expect(res.status).toBe(200) - expect(res.body).toEqual({ + expect(await res.json()).toEqual({ id: 1, name: 'Foo Bar', flag: true, }) - expect(res.headers['content-type']).toBe('application/json') + expect(res.headers.get('content-type')).toBe('application/json') }) it('Should return correct headers and data for text', async () => { - const res = await request(server).get('/static/plain.txt') + const res = await requestServer(server, { method: 'GET', path: '/static/plain.txt' }) const stats = statSync(path.join(__dirname, 'assets', 'static', 'plain.txt')) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('text/plain; charset=utf-8') - expect(res.headers['last-modified']).toBe(stats.mtime.toUTCString()) - expect(res.text).toBe('This is plain.txt') + expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8') + expect(res.headers.get('last-modified')).toBe(stats.mtime.toUTCString()) + expect(await res.text()).toBe('This is plain.txt') }) it('Should return 404 for non-existent files', async () => { - const res = await request(server).get('/static/does-not-exist.html') + const res = await requestServer(server, { + method: 'GET', + path: '/static/does-not-exist.html', + }) expect(res.status).toBe(404) - expect(res.headers['content-type']).toBe('text/plain; charset=UTF-8') - expect(res.text).toBe('404 Not Found') + expect(res.headers.get('content-type')).toBe('text/plain; charset=UTF-8') + expect(await res.text()).toBe('404 Not Found') }) it('Should return 200 with rewriteRequestPath', async () => { - const res = await request(server).get('/dot-static/plain.txt') + const res = await requestServer(server, { method: 'GET', path: '/dot-static/plain.txt' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('text/plain; charset=utf-8') - expect(res.text).toBe('This is plain.txt') + expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8') + expect(await res.text()).toBe('This is plain.txt') }) it('Should return 404 with rewriteRequestPath', async () => { - const res = await request(server).get('/dot-static/does-no-exists.txt') + const res = await requestServer(server, { + method: 'GET', + path: '/dot-static/does-no-exists.txt', + }) expect(res.status).toBe(404) }) it('Should return 200 with rewriteRequestPath with the context', async () => { - const res = await request(server).get('/static-with-context-path-route/plain.txt') + const res = await requestServer(server, { + method: 'GET', + path: '/static-with-context-path-route/plain.txt', + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('text/plain; charset=utf-8') - expect(res.text).toBe('This is plain.txt') + expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8') + expect(await res.text()).toBe('This is plain.txt') }) it('Should return 200 response to HEAD request', async () => { - const res = await request(server).head('/static/plain.txt') + const res = await requestServer(server, { method: 'HEAD', path: '/static/plain.txt' }) const stats = statSync(path.join(__dirname, 'assets', 'static', 'plain.txt')) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('text/plain; charset=utf-8') - expect(res.headers['content-length']).toBe('17') - expect(res.headers['last-modified']).toBe(stats.mtime.toUTCString()) - expect(res.text).toBe(undefined) + expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8') + expect(res.headers.get('content-length')).toBe('17') + expect(res.headers.get('last-modified')).toBe(stats.mtime.toUTCString()) + expect(res.body).toBeNull() }) it('Should return correct headers and data with range headers', async () => { - let res = await request(server).get('/static/plain.txt').set('range', '0-9') + let res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range: '0-9' }, + }) const stats = statSync(path.join(__dirname, 'assets', 'static', 'plain.txt')) expect(res.status).toBe(206) - expect(res.headers['content-type']).toBe('text/plain; charset=utf-8') - expect(res.headers['content-length']).toBe('10') - expect(res.headers['content-range']).toBe('bytes 0-9/17') - expect(res.headers['last-modified']).toBe(stats.mtime.toUTCString()) - expect(res.headers['date']).not.toBe(stats.mtime.toUTCString()) - expect(res.headers['date']).not.toBe(stats.birthtime.toUTCString()) - expect(res.text.length).toBe(10) - expect(res.text).toBe('This is pl') - - res = await request(server).get('/static/plain.txt').set('range', '10-16') + expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8') + expect(res.headers.get('content-length')).toBe('10') + expect(res.headers.get('content-range')).toBe('bytes 0-9/17') + expect(res.headers.get('last-modified')).toBe(stats.mtime.toUTCString()) + expect(res.headers.get('date')).not.toBe(stats.mtime.toUTCString()) + expect(res.headers.get('date')).not.toBe(stats.birthtime.toUTCString()) + let text = await res.text() + expect(text.length).toBe(10) + expect(text).toBe('This is pl') + + res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range: '10-16' }, + }) expect(res.status).toBe(206) - expect(res.headers['content-type']).toBe('text/plain; charset=utf-8') - expect(res.headers['content-length']).toBe('7') - expect(res.headers['content-range']).toBe('bytes 10-16/17') - expect(res.text.length).toBe(7) - expect(res.text).toBe('ain.txt') + expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8') + expect(res.headers.get('content-length')).toBe('7') + expect(res.headers.get('content-range')).toBe('bytes 10-16/17') + text = await res.text() + expect(text.length).toBe(7) + expect(text).toBe('ain.txt') }) it('Should return correct headers and data if client range exceeds the data size', async () => { - const res = await request(server).get('/static/plain.txt').set('range', '0-20') + const res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range: '0-20' }, + }) expect(res.status).toBe(206) - expect(res.headers['content-type']).toBe('text/plain; charset=utf-8') - expect(res.headers['content-length']).toBe('17') - expect(res.headers['content-range']).toBe('bytes 0-16/17') - expect(res.text.length).toBe(17) - expect(res.text).toBe('This is plain.txt') + expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8') + expect(res.headers.get('content-length')).toBe('17') + expect(res.headers.get('content-range')).toBe('bytes 0-16/17') + const text = await res.text() + expect(text.length).toBe(17) + expect(text).toBe('This is plain.txt') }) it('Should handle invalid range header gracefully without NaN error', async () => { - const res = await request(server).get('/static/plain.txt').set('range', 'hello') + const res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range: 'hello' }, + }) expect(res.status).toBe(206) - expect(res.headers['content-length']).toBe('17') - expect(res.headers['content-range']).toBe('bytes 0-16/17') + expect(res.headers.get('content-length')).toBe('17') + expect(res.headers.get('content-range')).toBe('bytes 0-16/17') }) it('Should return the last N bytes for a suffix range', async () => { - const res = await request(server).get('/static/plain.txt').set('range', 'bytes=-5') + const res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range: 'bytes=-5' }, + }) expect(res.status).toBe(206) - expect(res.headers['content-length']).toBe('5') - expect(res.headers['content-range']).toBe('bytes 12-16/17') - expect(res.text).toBe('n.txt') + expect(res.headers.get('content-length')).toBe('5') + expect(res.headers.get('content-range')).toBe('bytes 12-16/17') + expect(await res.text()).toBe('n.txt') }) it('Should return the whole file for a suffix range exceeding the file size', async () => { - const res = await request(server).get('/static/plain.txt').set('range', 'bytes=-100') + const res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range: 'bytes=-100' }, + }) expect(res.status).toBe(206) - expect(res.headers['content-range']).toBe('bytes 0-16/17') - expect(res.text).toBe('This is plain.txt') + expect(res.headers.get('content-range')).toBe('bytes 0-16/17') + expect(await res.text()).toBe('This is plain.txt') }) it('Should return exactly 1 byte for range bytes=0-0', async () => { - const res = await request(server).get('/static/plain.txt').set('range', 'bytes=0-0') + const res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range: 'bytes=0-0' }, + }) expect(res.status).toBe(206) - expect(res.headers['content-length']).toBe('1') - expect(res.headers['content-range']).toBe('bytes 0-0/17') - expect(res.text).toBe('T') + expect(res.headers.get('content-length')).toBe('1') + expect(res.headers.get('content-range')).toBe('bytes 0-0/17') + expect(await res.text()).toBe('T') }) it('Should return 416 when the range start is beyond the end of the file', async () => { - const res = await request(server).get('/static/plain.txt').set('range', 'bytes=100-200') + const res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range: 'bytes=100-200' }, + }) expect(res.status).toBe(416) - expect(res.headers['content-range']).toBe('bytes */17') + expect(res.headers.get('content-range')).toBe('bytes */17') }) it('Should return 416 when the range start is beyond the file size, even if the window is small', async () => { - const res = await request(server).get('/static/plain.txt').set('range', 'bytes=20-25') + const res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range: 'bytes=20-25' }, + }) expect(res.status).toBe(416) - expect(res.headers['content-range']).toBe('bytes */17') + expect(res.headers.get('content-range')).toBe('bytes */17') }) it('Should return 416 when the range start is after the range end', async () => { - const res = await request(server).get('/static/plain.txt').set('range', 'bytes=10-5') + const res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range: 'bytes=10-5' }, + }) expect(res.status).toBe(416) - expect(res.headers['content-range']).toBe('bytes */17') + expect(res.headers.get('content-range')).toBe('bytes */17') }) it('Should return 416 for a zero-length suffix range', async () => { - const res = await request(server).get('/static/plain.txt').set('range', 'bytes=-0') + const res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range: 'bytes=-0' }, + }) expect(res.status).toBe(416) - expect(res.headers['content-range']).toBe('bytes */17') + expect(res.headers.get('content-range')).toBe('bytes */17') }) it.each(['bytes=0-1x', 'bytes=x-1', 'bytes=0-1-2', 'bytes=-5-extra'])( 'Should treat a malformed range as the whole file: %s', async (range) => { - const res = await request(server).get('/static/plain.txt').set('range', range) + const res = await requestServer(server, { + method: 'GET', + path: '/static/plain.txt', + headers: { range }, + }) expect(res.status).toBe(206) - expect(res.headers['content-range']).toBe('bytes 0-16/17') - expect(res.text).toBe('This is plain.txt') + expect(res.headers.get('content-range')).toBe('bytes 0-16/17') + expect(await res.text()).toBe('This is plain.txt') } ) it('Should return 416 instead of crashing for a range request on an empty file', async () => { - const res = await request(server).get('/static/foo..bar.txt').set('range', 'bytes=0-0') + const res = await requestServer(server, { + method: 'GET', + path: '/static/foo..bar.txt', + headers: { range: 'bytes=0-0' }, + }) expect(res.status).toBe(416) - expect(res.headers['content-range']).toBe('bytes */0') + expect(res.headers.get('content-range')).toBe('bytes */0') }) it('Should return 416 instead of crashing for a malformed range on an empty file', async () => { - const res = await request(server).get('/static/foo..bar.txt').set('range', 'hello') + const res = await requestServer(server, { + method: 'GET', + path: '/static/foo..bar.txt', + headers: { range: 'hello' }, + }) expect(res.status).toBe(416) - expect(res.headers['content-range']).toBe('bytes */0') + expect(res.headers.get('content-range')).toBe('bytes */0') }) it('Should handle the `onNotFound` option', async () => { - const res = await request(server).get('/on-not-found/foo.txt') + const res = await requestServer(server, { method: 'GET', path: '/on-not-found/foo.txt' }) expect(res.status).toBe(404) expect(notFoundMessage).toMatch( /not-found[\/\\]on-not-found[\/\\]foo\.txt is not found, request to \/on-not-found\/foo\.txt$/ @@ -279,11 +331,12 @@ describe('Serve Static Middleware', () => { }) it('Should handle the `onFound` option', async () => { - const res = await request(server).get( - '/static/data.json?type=application/json;%20charset=utf-8' - ) + const res = await requestServer(server, { + method: 'GET', + path: '/static/data.json?type=application/json;%20charset=utf-8', + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('application/json; charset=utf-8') + expect(res.headers.get('content-type')).toBe('application/json; charset=utf-8') }) it('Should handle double dots in URL', async () => { @@ -300,78 +353,92 @@ describe('Serve Static Middleware', () => { rmSync(symlinkPath, { force: true }) symlinkSync(symlinkTarget, symlinkPath) - const res = await request(server).get('/static/symlink.html') + const res = await requestServer(server, { method: 'GET', path: '/static/symlink.html' }) expect(res.status).toBe(200) - expect(res.text).toBe('

Hello Hono

') + expect(await res.text()).toBe('

Hello Hono

') } finally { rmSync(symlinkPath, { force: true }) } }) it('Should handle URIError thrown while decoding URI component', async () => { - const res = await request(server).get('/static/%c0%afsecret.txt') + const res = await requestServer(server, { method: 'GET', path: '/static/%c0%afsecret.txt' }) expect(res.status).toBe(404) }) it('Should handle an extension less files', async () => { - const res = await request(server).get('/static/extensionless') + const res = await requestServer(server, { method: 'GET', path: '/static/extensionless' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('application/octet-stream') - expect(res.body.toString()).toBe('Extensionless') + expect(res.headers.get('content-type')).toBe('application/octet-stream') + expect(await res.text()).toBe('Extensionless') }) it('Should return a pre-compressed zstd response - /static-with-precompressed/hello.txt', async () => { // Check if it returns a normal response - let res = await request(server).get('/static-with-precompressed/hello.txt') + let res = await requestServer(server, { + method: 'GET', + path: '/static-with-precompressed/hello.txt', + }) let stats = statSync(path.join(__dirname, 'assets', 'static-with-precompressed', 'hello.txt')) expect(res.status).toBe(200) - expect(res.headers['content-length']).toBe('20') - expect(res.headers['last-modified']).toBe(stats.mtime.toUTCString()) - expect(res.text).toBe('Hello Not Compressed') - - res = await request(server) - .get('/static-with-precompressed/hello.txt') - .set('Accept-Encoding', 'zstd') + expect(res.headers.get('content-length')).toBe('20') + expect(res.headers.get('last-modified')).toBe(stats.mtime.toUTCString()) + expect(await res.text()).toBe('Hello Not Compressed') + + res = await requestServer(server, { + method: 'GET', + path: '/static-with-precompressed/hello.txt', + headers: { 'accept-encoding': 'zstd' }, + }) stats = statSync(path.join(__dirname, 'assets', 'static-with-precompressed', 'hello.txt.zst')) expect(res.status).toBe(200) - expect(res.headers['content-length']).toBe('21') - expect(res.headers['content-encoding']).toBe('zstd') - expect(res.headers['last-modified']).toBe(stats.mtime.toUTCString()) - expect(res.headers['vary']).toBe('Accept-Encoding') - expect(res.text).toBe('Hello zstd Compressed') + expect(res.headers.get('content-length')).toBe('21') + expect(res.headers.get('content-encoding')).toBe('zstd') + expect(res.headers.get('last-modified')).toBe(stats.mtime.toUTCString()) + expect(res.headers.get('vary')).toBe('Accept-Encoding') + // the .zst asset is not really compressed, so the raw body can be asserted directly + expect(await res.text()).toBe('Hello zstd Compressed') }) it('Should return a pre-compressed brotli response - /static-with-precompressed/hello.txt', async () => { - const res = await request(server) - .get('/static-with-precompressed/hello.txt') - .set('Accept-Encoding', 'wompwomp, gzip, br, deflate, zstd') + const res = await requestServer(server, { + method: 'GET', + path: '/static-with-precompressed/hello.txt', + headers: { 'accept-encoding': 'wompwomp, gzip, br, deflate, zstd' }, + }) expect(res.status).toBe(200) - expect(res.headers['content-length']).toBe('23') - expect(res.headers['content-encoding']).toBe('br') - expect(res.headers['vary']).toBe('Accept-Encoding') - expect(res.text).toBe('Hello br Compressed') + expect(res.headers.get('content-length')).toBe('19') + expect(res.headers.get('content-encoding')).toBe('br') + expect(res.headers.get('vary')).toBe('Accept-Encoding') + // the .br asset is not really compressed, so the raw body can be asserted directly + expect(await res.text()).toBe('Hello br Compressed') }) it('Should not return a pre-compressed response - /static-with-precompressed/hello.txt', async () => { - const res = await request(server) - .get('/static-with-precompressed/hello.txt') - .set('Accept-Encoding', 'wompwomp, unknown') + const res = await requestServer(server, { + method: 'GET', + path: '/static-with-precompressed/hello.txt', + headers: { 'accept-encoding': 'wompwomp, unknown' }, + }) expect(res.status).toBe(200) - expect(res.headers['content-encoding']).toBeUndefined() - expect(res.headers['vary']).toBeUndefined() - expect(res.text).toBe('Hello Not Compressed') + expect(res.headers.get('content-encoding')).toBeNull() + expect(res.headers.get('vary')).toBeNull() + expect(await res.text()).toBe('Hello Not Compressed') }) it('Should return a pre-compressed response for an octet-stream file - /static-with-precompressed/hello.bin', async () => { - const res = await request(server) - .get('/static-with-precompressed/hello.bin') - .set('Accept-Encoding', 'gzip, br, zstd') + const res = await requestServer(server, { + method: 'GET', + path: '/static-with-precompressed/hello.bin', + headers: { 'accept-encoding': 'gzip, br, zstd' }, + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('application/octet-stream') - expect(res.headers['content-length']).toBe('23') - expect(res.headers['content-encoding']).toBe('br') - expect(res.headers['vary']).toBe('Accept-Encoding') - expect(res.body.toString()).toBe('Hello br Compressed') + expect(res.headers.get('content-type')).toBe('application/octet-stream') + expect(res.headers.get('content-length')).toBe('19') + expect(res.headers.get('content-encoding')).toBe('br') + expect(res.headers.get('vary')).toBe('Accept-Encoding') + // the .br asset is not really compressed, so the raw body can be asserted directly + expect(await res.text()).toBe('Hello br Compressed') }) describe('Absolute path', () => { @@ -387,22 +454,22 @@ describe('Serve Static Middleware', () => { app.use('/favicon.ico', serveStatic({ path: root + path.sep + 'favicon.ico' })) it('Should return index.html', async () => { - const res = await request(server).get('/static') + const res = await requestServer(server, { method: 'GET', path: '/static' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('text/html; charset=utf-8') - expect(res.text).toBe('

Hello Hono

') + expect(res.headers.get('content-type')).toBe('text/html; charset=utf-8') + expect(await res.text()).toBe('

Hello Hono

') }) it('Should return correct headers and data for text', async () => { - const res = await request(server).get('/static/plain.txt') + const res = await requestServer(server, { method: 'GET', path: '/static/plain.txt' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('text/plain; charset=utf-8') - expect(res.text).toBe('This is plain.txt') + expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8') + expect(await res.text()).toBe('This is plain.txt') }) it('Should return correct headers for icons', async () => { - const res = await request(server).get('/favicon.ico') + const res = await requestServer(server, { method: 'GET', path: '/favicon.ico' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('image/x-icon') + expect(res.headers.get('content-type')).toBe('image/x-icon') }) }) }) @@ -430,9 +497,9 @@ describe('Serve Static Middleware', () => { ) it('Should return 200 response if both root and path set', async () => { - const res = await request(server).get('/favicon.ico') + const res = await requestServer(server, { method: 'GET', path: '/favicon.ico' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toBe('image/x-icon') + expect(res.headers.get('content-type')).toBe('image/x-icon') }) }) }) @@ -444,21 +511,6 @@ describe('Serve Static Middleware', () => { const server = createAdaptorServer(app) app.use('/static/*', serveStatic({ root: './test/assets' })) - beforeAll( - () => - new Promise((resolve, reject) => { - server.once('error', reject) - server.listen(0, '127.0.0.1', resolve) - }) - ) - - afterAll( - () => - new Promise((resolve, reject) => { - server.close((error) => (error ? reject(error) : resolve())) - }) - ) - it('Should prevent path traversal attacks with double dots', async () => { const res = await requestServer(server, { method: 'GET', path: '/static/../secret.txt' }) expect(res.status).toBe(404) @@ -481,12 +533,15 @@ describe('Serve Static Middleware', () => { }) it('Should prevent path traversal attacks with encoded dots', async () => { - const res = await request(server).get('/static/%2e%2e%2fsecret.txt') + const res = await requestServer(server, { + method: 'GET', + path: '/static/%2e%2e%2fsecret.txt', + }) expect(res.status).toBe(404) }) it('Should accept filename with double dots', async () => { - const res = await request(server).get('/static/foo..bar.txt') + const res = await requestServer(server, { method: 'GET', path: '/static/foo..bar.txt' }) expect(res.status).toBe(200) }) }) @@ -503,22 +558,31 @@ describe('Serve Static Middleware', () => { app.use('/static/*', serveStatic({ root: './test/assets' })) it('Should not allow bypass via path mismatch between middleware and serveStatic', async () => { - const res = await request(server).get('/static/admin/secret.txt') - expect(res.headers['x-authorized']).toBe('true') - expect(res.text).toBe('secret') + const res = await requestServer(server, { method: 'GET', path: '/static/admin/secret.txt' }) + expect(res.headers.get('x-authorized')).toBe('true') + expect(await res.text()).toBe('secret') - const res2 = await request(server).get('/static/admin%2Fsecret.txt') + const res2 = await requestServer(server, { + method: 'GET', + path: '/static/admin%2Fsecret.txt', + }) expect(res2.status).toBe(404) - expect(res2.headers['x-authorized']).toBeUndefined() - expect(res2.text).not.toBe('secret') + expect(res2.headers.get('x-authorized')).toBeNull() + expect(await res2.text()).not.toBe('secret') - const res3 = await request(server).get('/static//admin/secret.txt') + const res3 = await requestServer(server, { + method: 'GET', + path: '/static//admin/secret.txt', + }) expect(res3.status).toBe(404) - const res4 = await request(server).get('/static/admin%5Csecret.txt') + const res4 = await requestServer(server, { + method: 'GET', + path: '/static/admin%5Csecret.txt', + }) expect(res4.status).toBe(404) - expect(res4.headers['x-authorized']).toBeUndefined() - expect(res4.text).not.toBe('secret') + expect(res4.headers.get('x-authorized')).toBeNull() + expect(await res4.text()).not.toBe('secret') }) }) @@ -545,7 +609,9 @@ describe('Serve Static Middleware', () => { const app = new Hono() app.use('/static/*', serveStatic({ root: './test/assets' })) const server = createAdaptorServer(app) - await expect(request(server).get('/static/plain.txt')).rejects.toThrow() + await expect( + requestServer(server, { method: 'GET', path: '/static/plain.txt' }) + ).rejects.toThrow() } ) }) diff --git a/test/server.test.ts b/test/server.test.ts index 2f5bdac9..1182a862 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -4,131 +4,130 @@ import { compress } from 'hono/compress' import { etag } from 'hono/etag' import { poweredBy } from 'hono/powered-by' import { stream } from 'hono/streaming' -import request from 'supertest' import fs from 'node:fs' import { createServer as createHttp2Server } from 'node:http2' import { createServer as createHTTPSServer } from 'node:https' +import { gunzipSync, inflateSync } from 'node:zlib' import { GlobalRequest, Request as LightweightRequest, getAbortController } from '../src/request' import { GlobalResponse, Response as LightweightResponse } from '../src/response' import { createAdaptorServer, serve } from '../src/server' import type { HttpBindings, ServerType } from '../src/types' import { app } from './app' +import { requestServer, requestServerChunked, requestServerHttp2 } from './helpers/request' describe('Basic', () => { const server = createAdaptorServer(app) it('Should return 200 response - GET /', async () => { - const res = await request(server).get('/') + const res = await requestServer(server, { method: 'GET', path: '/' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.text).toBe('Hello! Node!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(await res.text()).toBe('Hello! Node!') }) it('Should return 200 response - GET /url', async () => { - const res = await request(server).get('/url').trustLocalhost() + const res = await requestServer(server, { method: 'GET', path: '/url' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - const url = new URL(res.text) + expect(res.headers.get('content-type')).toMatch('text/plain') + const url = new URL(await res.text()) expect(url.pathname).toBe('/url') expect(url.hostname).toBe('127.0.0.1') expect(url.protocol).toBe('http:') }) it('Should return 200 response - GET /posts?page=2', async () => { - const res = await request(server).get('/posts?page=2') + const res = await requestServer(server, { method: 'GET', path: '/posts?page=2' }) expect(res.status).toBe(200) - expect(res.text).toBe('Page 2') + expect(await res.text()).toBe('Page 2') }) it('Should return 200 response - GET /user-agent', async () => { - const res = await request(server).get('/user-agent').set('user-agent', 'Hono') + const res = await requestServer(server, { + method: 'GET', + path: '/user-agent', + headers: { 'user-agent': 'Hono' }, + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.text).toBe('Hono') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(await res.text()).toBe('Hono') }) it('Should return 302 response - POST /posts', async () => { - const res = await request(server).post('/posts') + const res = await requestServer(server, { method: 'POST', path: '/posts' }) expect(res.status).toBe(302) - expect(res.headers['location']).toBe('/posts') + expect(res.headers.get('location')).toBe('/posts') }) it('Should return 200 response - POST /no-body-consumed', async () => { - const res = await request(server).post('/no-body-consumed').send('') + const res = await requestServer(server, { + method: 'POST', + path: '/no-body-consumed', + headers: { 'content-length': '0' }, + body: '', + }) expect(res.status).toBe(200) - expect(res.text).toBe('No body consumed') + expect(await res.text()).toBe('No body consumed') }) it('Should return 200 response - POST /body-cancelled', async () => { - const res = await request(server).post('/body-cancelled').send('') + const res = await requestServer(server, { + method: 'POST', + path: '/body-cancelled', + headers: { 'content-length': '0' }, + body: '', + }) expect(res.status).toBe(200) - expect(res.text).toBe('Body cancelled') + expect(await res.text()).toBe('Body cancelled') }) it('Should return 200 response - POST /partially-consumed', async () => { const buffer = Buffer.alloc(1024 * 10) // large buffer - const res = await new Promise((resolve, reject) => { - const req = request(server) - .post('/partially-consumed') - .set('Content-Length', buffer.length.toString()) - - req.write(buffer) - req.end((err, res) => { - if (err) { - reject(err) - } else { - resolve(res) - } - }) + const res = await requestServer(server, { + method: 'POST', + path: '/partially-consumed', + headers: { 'content-length': buffer.length.toString() }, + body: buffer, }) expect(res.status).toBe(200) - expect(res.text).toBe('Partially consumed') + expect(await res.text()).toBe('Partially consumed') }) it('Should return 200 response - POST /partially-consumed-and-cancelled', async () => { const buffer = Buffer.alloc(1) // A large buffer will not make the test go far, so keep it small because it won't go far. - const res = await new Promise((resolve, reject) => { - const req = request(server) - .post('/partially-consumed-and-cancelled') - .set('Content-Length', buffer.length.toString()) - - req.write(buffer) - req.end((err, res) => { - if (err) { - reject(err) - } else { - resolve(res) - } - }) + const res = await requestServer(server, { + method: 'POST', + path: '/partially-consumed-and-cancelled', + headers: { 'content-length': buffer.length.toString() }, + body: buffer, }) expect(res.status).toBe(200) - expect(res.text).toBe('Partially consumed and cancelled') + expect(await res.text()).toBe('Partially consumed and cancelled') }) it('Should return 201 response - DELETE /posts/123', async () => { - const res = await request(server).delete('/posts/123') + const res = await requestServer(server, { method: 'DELETE', path: '/posts/123' }) expect(res.status).toBe(200) - expect(res.text).toBe('DELETE 123') + expect(await res.text()).toBe('DELETE 123') }) it('Should return 500 response - GET /invalid', async () => { - const res = await request(server).get('/invalid') + const res = await requestServer(server, { method: 'GET', path: '/invalid' }) expect(res.status).toBe(500) - expect(res.headers['content-type']).toEqual('text/plain') + expect(res.headers.get('content-type')).toEqual('text/plain') }) it('Should return 200 response - GET /ponyfill', async () => { - const res = await request(server).get('/ponyfill') + const res = await requestServer(server, { method: 'GET', path: '/ponyfill' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.text).toBe('Pony') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(await res.text()).toBe('Pony') }) it('Should not raise error for TRACE method', async () => { - const res = await request(server).trace('/') - expect(res.text).toBe('headers: {}') + const res = await requestServer(server, { method: 'TRACE', path: '/' }) + expect(await res.text()).toBe('headers: {}') }) }) @@ -286,162 +285,168 @@ describe('various response body types', () => { }) it('Should return 200 response - GET /', async () => { - const res = await request(server).get('/') + const res = await requestServer(server, { method: 'GET', path: '/' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.headers['content-length']).toMatch('12') - expect(res.text).toBe('Hello! Node!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(res.headers.get('content-length')).toMatch('12') + expect(await res.text()).toBe('Hello! Node!') }) it('Should return 200 response - GET /large', async () => { - const res = await request(server).get('/large') + const res = await requestServer(server, { method: 'GET', path: '/large' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.headers['content-length']).toMatch(largeText.length.toString()) - expect(res.text).toBe(largeText) + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(res.headers.get('content-length')).toMatch(largeText.length.toString()) + expect(await res.text()).toBe(largeText) }) it('Should return 200 response - GET /uint8array', async () => { - const res = await request(server).get('/uint8array') + const res = await requestServer(server, { method: 'GET', path: '/uint8array' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('application/octet-stream') - expect(res.headers['content-length']).toMatch('3') - expect(res.body).toEqual(Buffer.from([1, 2, 3])) + expect(res.headers.get('content-type')).toMatch('application/octet-stream') + expect(res.headers.get('content-length')).toMatch('3') + expect(Buffer.from(await res.arrayBuffer())).toEqual(Buffer.from([1, 2, 3])) }) it('Should return 200 response - GET /blob', async () => { - const res = await request(server).get('/blob') + const res = await requestServer(server, { method: 'GET', path: '/blob' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('application/octet-stream') - expect(res.headers['content-length']).toMatch('3') - expect(res.body).toEqual(Buffer.from([1, 2, 3])) + expect(res.headers.get('content-type')).toMatch('application/octet-stream') + expect(res.headers.get('content-length')).toMatch('3') + expect(Buffer.from(await res.arrayBuffer())).toEqual(Buffer.from([1, 2, 3])) }) it('Should return 200 response - GET /readable-stream', async () => { - const expectedChunks = ['Hello!', ' Node!'] - const resPromise = request(server) - .get('/readable-stream') - .parse((res, fn) => { - // response header should be sent before sending data. - expect(res.headers['transfer-encoding']).toBe('chunked') - resolveReadableStreamPromise() - - res.on('data', (chunk) => { - const str = chunk.toString() - expect(str).toBe(expectedChunks.shift()) - }) - res.on('end', () => fn(null, '')) - }) - await new Promise((resolve) => setTimeout(resolve, 100)) - const res = await resPromise + const { chunks, response: res } = await requestServerChunked( + server, + { method: 'GET', path: '/readable-stream' }, + { + onHeaders: (headers) => { + // response header should be sent before sending data. + expect(headers.get('transfer-encoding')).toBe('chunked') + resolveReadableStreamPromise() + }, + } + ) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain; charset=UTF-8') - expect(res.headers['content-length']).toBeUndefined() - expect(expectedChunks.length).toBe(0) // all chunks are received + expect(res.headers.get('content-type')).toMatch('text/plain; charset=UTF-8') + expect(res.headers.get('content-length')).toBeNull() + expect(chunks.map((chunk) => chunk.toString())).toEqual(['Hello!', ' Node!']) }) it('Should return 200 response - GET /readable-stream-with-transfer-encoding', async () => { - const res = await request(server).get('/readable-stream-with-transfer-encoding') + const res = await requestServer(server, { + method: 'GET', + path: '/readable-stream-with-transfer-encoding', + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain; charset=UTF-8') - expect(res.headers['transfer-encoding']).toBe('chunked') - expect(res.headers['content-length']).toBeUndefined() + expect(res.headers.get('content-type')).toMatch('text/plain; charset=UTF-8') + expect(res.headers.get('transfer-encoding')).toBe('chunked') + expect(res.headers.get('content-length')).toBeNull() }) it('Should return 200 response - GET /event-stream', async () => { - const expectedChunks = ['data: First!\n\n', 'data: Second!\n\n'] - const resPromise = request(server) - .get('/event-stream') - .parse((res, fn) => { - // response header should be sent before sending data. - expect(res.headers['transfer-encoding']).toBe('chunked') - resolveEventStreamPromise() - - res.on('data', (chunk) => { - const str = chunk.toString() - expect(str).toBe(expectedChunks.shift()) - }) - res.on('end', () => fn(null, '')) - }) - await new Promise((resolve) => setTimeout(resolve, 100)) - const res = await resPromise + const { chunks, response: res } = await requestServerChunked( + server, + { method: 'GET', path: '/event-stream' }, + { + onHeaders: (headers) => { + // response header should be sent before sending data. + expect(headers.get('transfer-encoding')).toBe('chunked') + resolveEventStreamPromise() + }, + } + ) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/event-stream') - expect(res.headers['content-length']).toBeUndefined() - expect(expectedChunks.length).toBe(0) // all chunks are received + expect(res.headers.get('content-type')).toMatch('text/event-stream') + expect(res.headers.get('content-length')).toBeNull() + expect(chunks.map((chunk) => chunk.toString())).toEqual([ + 'data: First!\n\n', + 'data: Second!\n\n', + ]) }) it('Should return 200 response - GET /event-stream-without-transfer-encoding', async () => { - const expectedChunks = ['data: First!\n\n', 'data: Second!\n\n'] - const resPromise = request(server) - .get('/event-stream-without-transfer-encoding') - .parse((res, fn) => { - res.on('data', (chunk) => { - const str = chunk.toString() - expect(str).toBe(expectedChunks.shift()) - - if (expectedChunks.length === 1) { + const { chunks, response: res } = await requestServerChunked( + server, + { method: 'GET', path: '/event-stream-without-transfer-encoding' }, + { + onChunk: (_chunk, index) => { + if (index === 0) { // receive first chunk resolveEventStreamWithoutTransferEncodingPromise() } - }) - res.on('end', () => fn(null, '')) - }) - await new Promise((resolve) => setTimeout(resolve, 100)) - const res = await resPromise + }, + } + ) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/event-stream') - expect(res.headers['content-length']).toBeUndefined() - expect(expectedChunks.length).toBe(0) // all chunks are received + expect(res.headers.get('content-type')).toMatch('text/event-stream') + expect(res.headers.get('content-length')).toBeNull() + expect(chunks.map((chunk) => chunk.toString())).toEqual([ + 'data: First!\n\n', + 'data: Second!\n\n', + ]) }) it('Should return 200 response - GET /buffer', async () => { - const res = await request(server).get('/buffer') + const res = await requestServer(server, { method: 'GET', path: '/buffer' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.headers['content-length']).toMatch('11') - expect(res.text).toBe('Hello Hono!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(res.headers.get('content-length')).toMatch('11') + expect(await res.text()).toBe('Hello Hono!') }) it('Should return 200 response - GET /text-with-content-length-object', async () => { - const res = await request(server).get('/text-with-content-length-object') + const res = await requestServer(server, { + method: 'GET', + path: '/text-with-content-length-object', + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.headers['content-length']).toBe('00011') - expect(res.text).toBe('Hello Hono!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(res.headers.get('content-length')).toBe('00011') + expect(await res.text()).toBe('Hello Hono!') }) it('Should return 200 response - GET /text-with-content-length-headers', async () => { - const res = await request(server).get('/text-with-content-length-headers') + const res = await requestServer(server, { + method: 'GET', + path: '/text-with-content-length-headers', + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.headers['content-length']).toBe('00011') - expect(res.text).toBe('Hello Hono!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(res.headers.get('content-length')).toBe('00011') + expect(await res.text()).toBe('Hello Hono!') }) it('Should return 200 response - GET /text-with-content-length-array', async () => { - const res = await request(server).get('/text-with-content-length-array') + const res = await requestServer(server, { + method: 'GET', + path: '/text-with-content-length-array', + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.headers['content-length']).toBe('00011') - expect(res.text).toBe('Hello Hono!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(res.headers.get('content-length')).toBe('00011') + expect(await res.text()).toBe('Hello Hono!') }) it('Should return 200 response - GET /text-with-set-cookie-array', async () => { - const res = await request(server).get('/text-with-set-cookie-array') + const res = await requestServer(server, { + method: 'GET', + path: '/text-with-set-cookie-array', + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.headers['set-cookie']).toEqual(['a=1', 'b=2']) - expect(res.text).toBe('Hello Hono!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(res.headers.getSetCookie()).toEqual(['a=1', 'b=2']) + expect(await res.text()).toBe('Hello Hono!') }) it('Should return 200 response - GET /etag/buffer', async () => { - const res = await request(server).get('/etag/buffer') + const res = await requestServer(server, { method: 'GET', path: '/etag/buffer' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.headers['etag']).toMatch('"7e03b9b8ed6156932691d111c81c34c3c02912f9"') - expect(res.headers['content-length']).toMatch('11') - expect(res.text).toBe('Hello Hono!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(res.headers.get('etag')).toMatch('"7e03b9b8ed6156932691d111c81c34c3c02912f9"') + expect(res.headers.get('content-length')).toMatch('11') + expect(await res.text()).toBe('Hello Hono!') }) } @@ -469,17 +474,17 @@ describe('Routing', () => { const server = createAdaptorServer(app) it('Should return responses from `/book/*`', async () => { - let res = await request(server).get('/book') + let res = await requestServer(server, { method: 'GET', path: '/book' }) expect(res.status).toBe(200) - expect(res.text).toBe('get /book') + expect(await res.text()).toBe('get /book') - res = await request(server).get('/book/123') + res = await requestServer(server, { method: 'GET', path: '/book/123' }) expect(res.status).toBe(200) - expect(res.text).toBe('get /book/123') + expect(await res.text()).toBe('get /book/123') - res = await request(server).post('/book') + res = await requestServer(server, { method: 'POST', path: '/book' }) expect(res.status).toBe(200) - expect(res.text).toBe('post /book') + expect(await res.text()).toBe('post /book') }) }) @@ -498,15 +503,15 @@ describe('Routing', () => { const server = createAdaptorServer(app) it('Should return responses from `/chained/*`', async () => { - let res = await request(server).get('/chained/abc') + let res = await requestServer(server, { method: 'GET', path: '/chained/abc' }) expect(res.status).toBe(200) - expect(res.text).toBe('GET for abc') + expect(await res.text()).toBe('GET for abc') - res = await request(server).post('/chained/abc') + res = await requestServer(server, { method: 'POST', path: '/chained/abc' }) expect(res.status).toBe(200) - expect(res.text).toBe('POST for abc') + expect(await res.text()).toBe('POST for abc') - res = await request(server).put('/chained/abc') + res = await requestServer(server, { method: 'PUT', path: '/chained/abc' }) expect(res.status).toBe(404) }) }) @@ -525,19 +530,25 @@ describe('Request body', () => { const server = createAdaptorServer(app) it('Should handle JSON body', async () => { - const res = await request(server) - .post('/json') - .set('Content-Type', 'application/json') - .send({ foo: 'bar' }) + const res = await requestServer(server, { + method: 'POST', + path: '/json', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ foo: 'bar' }), + }) expect(res.status).toBe(200) - expect(JSON.parse(res.text)).toEqual({ foo: 'bar' }) + expect(await res.json()).toEqual({ foo: 'bar' }) }) it('Should handle form body', async () => { - // to be `application/x-www-form-urlencoded` - const res = await request(server).post('/form').type('form').send({ foo: 'bar' }) + const res = await requestServer(server, { + method: 'POST', + path: '/form', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: 'foo=bar', + }) expect(res.status).toBe(200) - expect(JSON.parse(res.text)).toEqual({ foo: 'bar' }) + expect(await res.json()).toEqual({ foo: 'bar' }) }) }) @@ -559,31 +570,31 @@ describe('Response body', () => { const server = createAdaptorServer(app) it('Should return JSON body', async () => { - const res = await request(server).get('/json') + const res = await requestServer(server, { method: 'GET', path: '/json' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('application/json') - expect(JSON.parse(res.text)).toEqual({ foo: 'bar' }) + expect(res.headers.get('content-type')).toMatch('application/json') + expect(await res.json()).toEqual({ foo: 'bar' }) }) it('Should return JSON body from /json-async', async () => { - const res = await request(server).get('/json-async') + const res = await requestServer(server, { method: 'GET', path: '/json-async' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('application/json') - expect(JSON.parse(res.text)).toEqual({ foo: 'async' }) + expect(res.headers.get('content-type')).toMatch('application/json') + expect(await res.json()).toEqual({ foo: 'async' }) }) it('Should return HTML', async () => { - const res = await request(server).get('/html') + const res = await requestServer(server, { method: 'GET', path: '/html' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/html') - expect(res.text).toBe('

Hello!

') + expect(res.headers.get('content-type')).toMatch('text/html') + expect(await res.text()).toBe('

Hello!

') }) it('Should return HTML from /html-async', async () => { - const res = await request(server).get('/html-async') + const res = await requestServer(server, { method: 'GET', path: '/html-async' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/html') - expect(res.text).toBe('

Hello!

') + expect(res.headers.get('content-type')).toMatch('text/html') + expect(await res.text()).toBe('

Hello!

') }) }) @@ -605,17 +616,17 @@ describe('Response body', () => { const server = createAdaptorServer(app) it('Should return JSON body from /json-blob', async () => { - const res = await request(server).get('/json-blob') + const res = await requestServer(server, { method: 'GET', path: '/json-blob' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('application/json') - expect(JSON.parse(res.text)).toEqual({ foo: 'blob' }) + expect(res.headers.get('content-type')).toMatch('application/json') + expect(await res.json()).toEqual({ foo: 'blob' }) }) it('Should return JSON body from /json-buffer', async () => { - const res = await request(server).get('/json-buffer') + const res = await requestServer(server, { method: 'GET', path: '/json-buffer' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('application/json') - expect(JSON.parse(res.text)).toEqual({ foo: 'buffer' }) + expect(res.headers.get('content-type')).toMatch('application/json') + expect(await res.json()).toEqual({ foo: 'buffer' }) }) }) }) @@ -632,10 +643,10 @@ describe('Middleware', () => { const server = createAdaptorServer(app) it('Should have correct header values', async () => { - const res = await request(server).get('/') + const res = await requestServer(server, { method: 'GET', path: '/' }) expect(res.status).toBe(200) - expect(res.headers['x-powered-by']).toBe('Hono') - expect(res.headers['foo']).toBe('bar') + expect(res.headers.get('x-powered-by')).toBe('Hono') + expect(res.headers.get('foo')).toBe('bar') }) }) @@ -653,19 +664,19 @@ describe('Error handling', () => { const server = createAdaptorServer(app) it('Should return 404 response', async () => { - const res = await request(server).get('/') + const res = await requestServer(server, { method: 'GET', path: '/' }) expect(res.status).toBe(404) - expect(res.text).toBe('Custom NotFound') + expect(await res.text()).toBe('Custom NotFound') }) it('Should return 500 response', async () => { - const res = await request(server).get('/error') + const res = await requestServer(server, { method: 'GET', path: '/error' }) expect(res.status).toBe(500) - expect(res.text).toBe('Custom Error!') + expect(await res.text()).toBe('Custom Error!') }) it('Should return 404 response - PURGE method', async () => { - const res = await request(server).purge('/') + const res = await requestServer(server, { method: 'PURGE', path: '/' }) expect(res.status).toBe(404) }) }) @@ -698,25 +709,31 @@ describe('Basic Auth Middleware', () => { const server = createAdaptorServer(app) it('Should not authorized', async () => { - const res = await request(server).get('/auth/a') + const res = await requestServer(server, { method: 'GET', path: '/auth/a' }) expect(res.status).toBe(401) - expect(res.text).toBe('Unauthorized') + expect(await res.text()).toBe('Unauthorized') }) it('Should authorized', async () => { const credential = Buffer.from(username + ':' + password).toString('base64') - const res = await request(server).get('/auth/a').set('Authorization', `Basic ${credential}`) + const res = await requestServer(server, { + method: 'GET', + path: '/auth/a', + headers: { authorization: `Basic ${credential}` }, + }) expect(res.status).toBe(200) - expect(res.text).toBe('auth') + expect(await res.text()).toBe('auth') }) it('Should authorize Unicode', async () => { const credential = Buffer.from(username + ':' + unicodePassword).toString('base64') - const res = await request(server) - .get('/auth-unicode/a') - .set('Authorization', `Basic ${credential}`) + const res = await requestServer(server, { + method: 'GET', + path: '/auth-unicode/a', + headers: { authorization: `Basic ${credential}` }, + }) expect(res.status).toBe(200) - expect(res.text).toBe('auth') + expect(await res.text()).toBe('auth') }) }) @@ -763,50 +780,44 @@ describe('Stream and non-stream response', () => { const server = createAdaptorServer(app) it('Should return JSON body', async () => { - const res = await request(server).get('/json') + const res = await requestServer(server, { method: 'GET', path: '/json' }) expect(res.status).toBe(200) - expect(res.headers['content-length']).toMatch('13') - expect(res.headers['content-type']).toMatch('application/json') - expect(JSON.parse(res.text)).toEqual({ foo: 'bar' }) + expect(res.headers.get('content-length')).toMatch('13') + expect(res.headers.get('content-type')).toMatch('application/json') + expect(await res.json()).toEqual({ foo: 'bar' }) }) it('Should return text body', async () => { - const res = await request(server).get('/text') + const res = await requestServer(server, { method: 'GET', path: '/text' }) expect(res.status).toBe(200) - expect(res.headers['content-length']).toMatch('6') - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.text).toBe('Hello!') + expect(res.headers.get('content-length')).toMatch('6') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(await res.text()).toBe('Hello!') }) it('Should return JSON body - stream', async () => { - const res = await request(server).get('/json-stream') + const res = await requestServer(server, { method: 'GET', path: '/json-stream' }) expect(res.status).toBe(200) - expect(res.headers['content-length']).toBeUndefined() - expect(res.headers['content-type']).toMatch('application/json') - expect(res.headers['transfer-encoding']).toMatch('chunked') - expect(JSON.parse(res.text)).toEqual({ foo: 'bar' }) + expect(res.headers.get('content-length')).toBeNull() + expect(res.headers.get('content-type')).toMatch('application/json') + expect(res.headers.get('transfer-encoding')).toMatch('chunked') + expect(await res.json()).toEqual({ foo: 'bar' }) }) it('Should return text body - stream', async () => { - const res = await request(server) - .get('/stream') - .parse((res, fn) => { - const chunks: string[] = ['data: Hello!\n\n', 'data: end\n\n'] - let index = 0 - res.on('data', (chunk) => { - const str = chunk.toString() - expect(str).toBe(chunks[index++]) - }) - res.on('end', () => fn(null, '')) - }) + const { chunks, response: res } = await requestServerChunked(server, { + method: 'GET', + path: '/stream', + }) expect(res.status).toBe(200) - expect(res.headers['content-length']).toBeUndefined() - expect(res.headers['content-type']).toMatch('text/event-stream') - expect(res.headers['transfer-encoding']).toMatch('chunked') + expect(res.headers.get('content-length')).toBeNull() + expect(res.headers.get('content-type')).toMatch('text/event-stream') + expect(res.headers.get('transfer-encoding')).toMatch('chunked') + expect(chunks.map((chunk) => chunk.toString())).toEqual(['data: Hello!\n\n', 'data: end\n\n']) }) it('Should return error - stream without app crashing', async () => { - const result = request(server).get('/error-stream') + const result = requestServer(server, { method: 'GET', path: '/error-stream' }) await expect(result).rejects.toThrow('aborted') }) }) @@ -826,17 +837,17 @@ describe('SSL', () => { }) it('Should return 200 response - GET /', async () => { - const res = await request(server).get('/').trustLocalhost() + const res = await requestServer(server, { method: 'GET', path: '/' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.text).toBe('Hello! Node!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(await res.text()).toBe('Hello! Node!') }) it('Should return 200 response - GET /url', async () => { - const res = await request(server).get('/url').trustLocalhost() + const res = await requestServer(server, { method: 'GET', path: '/url' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - const url = new URL(res.text) + expect(res.headers.get('content-type')).toMatch('text/plain') + const url = new URL(await res.text()) expect(url.pathname).toBe('/url') expect(url.hostname).toBe('127.0.0.1') expect(url.protocol).toBe('https:') @@ -859,29 +870,29 @@ describe('HTTP2', () => { }) it('Should return 200 response - GET /', async () => { - const res = await request(server, { http2: true }).get('/').trustLocalhost() + const res = await requestServerHttp2(server, { method: 'GET', path: '/' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.text).toBe('Hello! Node!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(await res.text()).toBe('Hello! Node!') }) it('Should return 200 response - GET /headers', async () => { - const res = await request(server, { http2: true }).get('/headers').trustLocalhost() + const res = await requestServerHttp2(server, { method: 'GET', path: '/headers' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.text).toBe('Hello! Node!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(await res.text()).toBe('Hello! Node!') }) // Use :authority as the host for the url. it('Should return 200 response - GET /url', async () => { - const res = await request(server, { http2: true }) - .get('/url') - .set(':scheme', 'https') - .set(':authority', '127.0.0.1') - .trustLocalhost() + const res = await requestServerHttp2(server, { + method: 'GET', + path: '/url', + headers: { ':authority': '127.0.0.1', ':scheme': 'https' }, + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - const url = new URL(res.text) + expect(res.headers.get('content-type')).toMatch('text/plain') + const url = new URL(await res.text()) expect(url.pathname).toBe('/url') expect(url.hostname).toBe('127.0.0.1') expect(url.protocol).toBe('https:') @@ -915,28 +926,40 @@ describe('Hono compression default gzip', () => { it('should return 200 response - GET /one', async () => { const server = createAdaptorServer(app) - const res = await request(server).get('/one') + const res = await requestServer(server, { + method: 'GET', + path: '/one', + headers: { 'accept-encoding': 'gzip, deflate' }, + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.headers['content-encoding']).toMatch('gzip') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(res.headers.get('content-encoding')).toMatch('gzip') }) it('should return 404 Custom NotFound', async () => { const server = createAdaptorServer(app) - const res = await request(server).get('/err') + const res = await requestServer(server, { + method: 'GET', + path: '/err', + headers: { 'accept-encoding': 'gzip, deflate' }, + }) expect(res.status).toBe(404) - expect(res.text).toEqual('Custom NotFound') - expect(res.headers['content-type']).toEqual('text/plain; charset=UTF-8') - expect(res.headers['content-encoding']).toMatch('gzip') + expect(gunzipSync(Buffer.from(await res.arrayBuffer())).toString()).toEqual('Custom NotFound') + expect(res.headers.get('content-type')).toEqual('text/plain; charset=UTF-8') + expect(res.headers.get('content-encoding')).toMatch('gzip') }) it('should return 500 Custom Error!', async () => { const server = createAdaptorServer(app) - const res = await request(server).get('/error') + const res = await requestServer(server, { + method: 'GET', + path: '/error', + headers: { 'accept-encoding': 'gzip, deflate' }, + }) expect(res.status).toBe(500) - expect(res.text).toEqual('Custom Error!') - expect(res.headers['content-type']).toEqual('text/plain; charset=UTF-8') - expect(res.headers['content-encoding']).toMatch('gzip') + expect(gunzipSync(Buffer.from(await res.arrayBuffer())).toString()).toEqual('Custom Error!') + expect(res.headers.get('content-type')).toEqual('text/plain; charset=UTF-8') + expect(res.headers.get('content-encoding')).toMatch('gzip') }) }) @@ -967,28 +990,40 @@ describe('Hono compression deflate', () => { it('should return 200 response - GET /one', async () => { const server = createAdaptorServer(app) - const res = await request(server).get('/one') + const res = await requestServer(server, { + method: 'GET', + path: '/one', + headers: { 'accept-encoding': 'gzip, deflate' }, + }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.headers['content-encoding']).toMatch('deflate') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(res.headers.get('content-encoding')).toMatch('deflate') }) it('should return 404 Custom NotFound', async () => { const server = createAdaptorServer(app) - const res = await request(server).get('/err') + const res = await requestServer(server, { + method: 'GET', + path: '/err', + headers: { 'accept-encoding': 'gzip, deflate' }, + }) expect(res.status).toBe(404) - expect(res.text).toEqual('Custom NotFound') - expect(res.headers['content-type']).toEqual('text/plain; charset=UTF-8') - expect(res.headers['content-encoding']).toMatch('deflate') + expect(inflateSync(Buffer.from(await res.arrayBuffer())).toString()).toEqual('Custom NotFound') + expect(res.headers.get('content-type')).toEqual('text/plain; charset=UTF-8') + expect(res.headers.get('content-encoding')).toMatch('deflate') }) it('should return 500 Custom Error!', async () => { const server = createAdaptorServer(app) - const res = await request(server).get('/error') + const res = await requestServer(server, { + method: 'GET', + path: '/error', + headers: { 'accept-encoding': 'gzip, deflate' }, + }) expect(res.status).toBe(500) - expect(res.text).toEqual('Custom Error!') - expect(res.headers['content-type']).toEqual('text/plain; charset=UTF-8') - expect(res.headers['content-encoding']).toMatch('deflate') + expect(inflateSync(Buffer.from(await res.arrayBuffer())).toString()).toEqual('Custom Error!') + expect(res.headers.get('content-type')).toEqual('text/plain; charset=UTF-8') + expect(res.headers.get('content-encoding')).toMatch('deflate') }) }) @@ -1006,9 +1041,9 @@ describe('set child response to c.res', () => { it('Should return 200 response - GET /json', async () => { const server = createAdaptorServer(app) - const res = await request(server).get('/json') + const res = await requestServer(server, { method: 'GET', path: '/json' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('application/json') + expect(res.headers.get('content-type')).toMatch('application/json') }) }) @@ -1033,11 +1068,11 @@ describe('Headers appended to a raw Response after construction (issue #304)', ( it('Should preserve the appended Set-Cookie header', async () => { const server = createAdaptorServer(app) - const res = await request(server).post('/test') + const res = await requestServer(server, { method: 'POST', path: '/test' }) expect(res.status).toBe(200) - expect(res.text).toBe('hello') - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.headers['set-cookie']).toEqual(['session=abc; Path=/; HttpOnly']) + expect(await res.text()).toBe('hello') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(res.headers.getSetCookie()).toEqual(['session=abc; Path=/; HttpOnly']) }) }) @@ -1054,13 +1089,14 @@ describe('forwarding IncomingMessage and ServerResponse in env', () => { it('Should add `incoming` and `outgoing` to env', async () => { const server = createAdaptorServer(app) - const res = await request(server).get('/') + const res = await requestServer(server, { method: 'GET', path: '/' }) expect(res.status).toBe(200) - expect(res.body.incoming).toBe('IncomingMessage') - expect(res.body.url).toBe('/') - expect(res.body.outgoing).toBe('ServerResponse') - expect(res.body.status).toBe(200) + const body = await res.json() + expect(body.incoming).toBe('IncomingMessage') + expect(body.url).toBe('/') + expect(body.outgoing).toBe('ServerResponse') + expect(body.status).toBe(200) }) }) @@ -1136,19 +1172,34 @@ describe('Memory leak test', () => { }) }) + // keep the server up across all tests so every request shares the same + // environment, as the GC assertions are sensitive to their surroundings + beforeAll( + () => + new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', resolve) + }) + ) + afterAll(() => { server.close() }) it('Should not have memory leak - GET /', async () => { - await request(server).get('/') + await requestServer(server, { method: 'GET', path: '/' }) global.gc?.() await new Promise((resolve) => setTimeout(resolve, 10)) expect(counter).toBe(0) }) it('Should not have memory leak - POST /', async () => { - await request(server).post('/').set('Content-Type', 'application/json').send({ foo: 'bar' }) + await requestServer(server, { + method: 'POST', + path: '/', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ foo: 'bar' }), + }) global.gc?.() await new Promise((resolve) => setTimeout(resolve, 10)) expect(counter).toBe(0) @@ -1159,12 +1210,16 @@ describe('Memory leak test', () => { onAbort = resolve }) - const req = request(server) - .get('/abort') - .end(() => {}) + const controller = new AbortController() + const resPromise = requestServer(server, { + method: 'GET', + path: '/abort', + signal: controller.signal, + }).catch(() => {}) await reqReadyPromise - req.abort() + controller.abort() await abortedPromise + await resPromise await new Promise((resolve) => setTimeout(resolve, 10)) global.gc?.() diff --git a/test/utils/response.test.ts b/test/utils/response.test.ts index 5ae3e38b..30b551c0 100644 --- a/test/utils/response.test.ts +++ b/test/utils/response.test.ts @@ -1,8 +1,8 @@ import { Hono } from 'hono' -import request from 'supertest' import type { HttpBindings } from '../../src/' import { createAdaptorServer } from '../../src/server' import { RESPONSE_ALREADY_SENT } from '../../src/utils/response' +import { requestServer } from '../helpers/request' describe('RESPONSE_ALREADY_SENT', () => { const app = new Hono<{ Bindings: HttpBindings }>() @@ -15,9 +15,9 @@ describe('RESPONSE_ALREADY_SENT', () => { const server = createAdaptorServer(app) it('Should return 200 response - GET /', async () => { - const res = await request(server).get('/') + const res = await requestServer(server, { method: 'GET', path: '/' }) expect(res.status).toBe(200) - expect(res.headers['content-type']).toMatch('text/plain') - expect(res.text).toBe('Hono!') + expect(res.headers.get('content-type')).toMatch('text/plain') + expect(await res.text()).toBe('Hono!') }) }) diff --git a/tsconfig.json b/tsconfig.json index 87f9a3be..612b9325 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { "target": "es2022", - "module": "commonjs", + "module": "ESNext", "declaration": true, - "moduleResolution": "Node", + "moduleResolution": "Bundler", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, From ca57e80a4f11bf60f14b91d3a8de61a2f156b8e3 Mon Sep 17 00:00:00 2001 From: BlankParticle Date: Tue, 21 Jul 2026 13:17:37 +0530 Subject: [PATCH 2/2] chore: format --- src/request.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/request.ts b/src/request.ts index 0a907c22..813d09e3 100644 --- a/src/request.ts +++ b/src/request.ts @@ -43,7 +43,9 @@ export class Request extends GlobalRequest { } } -export const newHeadersFromIncoming = (incoming: Pick) => { +export const newHeadersFromIncoming = ( + incoming: Pick +) => { const headerRecord: [string, string][] = [] const rawHeaders = incoming.rawHeaders for (let i = 0, len = rawHeaders.length; i < len; i += 2) {