From b0ac65a21c3e79bce10331369cf867dcae41139d Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Fri, 1 Sep 2023 19:13:38 -0700 Subject: [PATCH] Add tests --- .github/workflows/test.yml | 52 ++ .gitignore | 2 +- package-lock.json | 465 ++++++++++++++++++ package.json | 11 +- src/handlers/directory.ts | 2 +- src/handlers/file.ts | 42 +- src/responses.ts | 3 + src/util.ts | 5 +- src/worker.ts | 4 +- tests/README.md | 2 + tests/e2e/README.md | 18 + tests/e2e/directory.test.ts | 112 +++++ tests/e2e/file.test.ts | 149 ++++++ tests/e2e/index.test.ts | 2 + tests/e2e/test-data/R2_BUCKET/README.md | 2 + ...2fd134f82c2b3f872824a9aac500000cea511db274 | 1 + ...5145b5b10c87db0b71310736d500000cea51c8d30c | 1 + ...7e8eb6cc71b76f951576ddcfff00000cea50e229d4 | 1 + ...9628ff06a4b2c4cd7a15e9711800000cea51dc0fa8 | 1 + ...c3619d895b7fcfac1cbea3bae700000cea51681f30 | 1 + ...9c16d9c865684739f46029ba3900000cea5198fe20 | 1 + ...3b53644c20f30f90cc1bdc4ff300000cea51b2f884 | 1 + ...3e0391bd3b94cc0701c7ddb54100000cea520b6ca8 | 1 + ...791f115401e3b7d2a2a4cd597c00000cea51381330 | 1 + ...9ff344e19a7e4da43500357d7e00000cea5181ac34 | 1 + ...15548c998c4798e73ce8dca44e00000cea51509568 | 1 + ...34190b88c7c7cb7dd3d63364ca00000cea5084db80 | 1 + ...66979dcf3ac380ba0273de9de400000cea50ffe460 | 1 + ...0ac2fd80566a62d51df363c3a700000cea51ef17ec | 1 + ...48de4766cd903ce22f7485848900000cea50b18770 | 1 + tests/e2e/test-data/R2_BUCKET/db.sqlite | Bin 0 -> 28672 bytes tests/e2e/test-data/expected-html/README.md | 2 + tests/e2e/test-data/expected-html/dist.html | 47 ++ tests/tsconfig.json | 6 + tests/unit/README.md | 22 + tests/unit/index.test.ts | 1 + tests/unit/util.test.ts | 126 +++++ 37 files changed, 1068 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 tests/README.md create mode 100644 tests/e2e/README.md create mode 100644 tests/e2e/directory.test.ts create mode 100644 tests/e2e/file.test.ts create mode 100644 tests/e2e/index.test.ts create mode 100644 tests/e2e/test-data/R2_BUCKET/README.md create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/1e4de7990b4ef01257964a2f7d12b8551db3af2fd134f82c2b3f872824a9aac500000cea511db274 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/2b8de93bc9a59f7b74004036973e87ede6a8985145b5b10c87db0b71310736d500000cea51c8d30c create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/452c0bddf19b5647831280c01a090c815526157e8eb6cc71b76f951576ddcfff00000cea50e229d4 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/755deadb8510a4bcf8b632106ce3bb2884cbb19628ff06a4b2c4cd7a15e9711800000cea51dc0fa8 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/7637f4431321361e34e8bdba83dfc0b7c4aad8c3619d895b7fcfac1cbea3bae700000cea51681f30 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/7cd7aaa22139ae89634e0a4f695745e7d195ce9c16d9c865684739f46029ba3900000cea5198fe20 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/84ec646e6ee82c6a2b627a3c7988a1e630e43c3b53644c20f30f90cc1bdc4ff300000cea51b2f884 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/8dc8517f83d7d2ac8a3030da0eedd2c4956c4e3e0391bd3b94cc0701c7ddb54100000cea520b6ca8 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/9c101170154bc7872e5b93675a74706d03205b791f115401e3b7d2a2a4cd597c00000cea51381330 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/9fbf67cc8db78f5ca67b84b435be22b5a814b29ff344e19a7e4da43500357d7e00000cea5181ac34 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/b11cbe7ea145ec48630b0d87fbd25b304348a215548c998c4798e73ce8dca44e00000cea51509568 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/b2b1ba500a2c700706ab817da2f848631db25d34190b88c7c7cb7dd3d63364ca00000cea5084db80 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/b9e14d8b54ea621f6549fee94f557cdb43646966979dcf3ac380ba0273de9de400000cea50ffe460 create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/e62d82792f96282b1c49fb7cdc70824ff825620ac2fd80566a62d51df363c3a700000cea51ef17ec create mode 100644 tests/e2e/test-data/R2_BUCKET/blobs/f34b8641aebf56ffe850eaf5d26f9604418e4d48de4766cd903ce22f7485848900000cea50b18770 create mode 100644 tests/e2e/test-data/R2_BUCKET/db.sqlite create mode 100644 tests/e2e/test-data/expected-html/README.md create mode 100644 tests/e2e/test-data/expected-html/dist.html create mode 100644 tests/tsconfig.json create mode 100644 tests/unit/README.md create mode 100644 tests/unit/index.test.ts create mode 100644 tests/unit/util.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7130751 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,52 @@ +name: Tests + +on: + pull_request_target: + branches: + - main + +jobs: + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Git Checkout + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 + + - name: Cache Dependencies + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 + with: + path: | + ~/.npm + node_modules/.cache + key: ${{ runner.os }}-npm-${{ hashFiles('**/workflows/format.yml') }} + restore-keys: ${{ runner.os }}-npm- + + - name: Install dependencies + run: npm install + + - name: Run Tests + run: npm run test:unit + e2e-test: + name: E2E Tests + runs-on: ubuntu-latest + + steps: + - name: Git Checkout + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 + + - name: Cache Dependencies + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 + with: + path: | + ~/.npm + node_modules/.cache + key: ${{ runner.os }}-npm-${{ hashFiles('**/workflows/format.yml') }} + restore-keys: ${{ runner.os }}-npm- + + - name: Install dependencies + run: npm install + + - name: Run Tests + run: npm run test:e2e diff --git a/.gitignore b/.gitignore index 8f0ded4..ef5b808 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ node_modules/ .wrangler/ -dist/ \ No newline at end of file +dist/ diff --git a/package-lock.json b/package-lock.json index 75375b4..733f82c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,14 @@ "version": "0.0.0", "devDependencies": { "@cloudflare/workers-types": "^4.20230419.0", + "@types/node": "^20.5.8", "@typescript-eslint/eslint-plugin": "^6.5.0", "@typescript-eslint/parser": "^6.5.0", "eslint": "^8.48.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "prettier": "^3.0.1", + "tsx": "^3.12.8", "typescript": "^5.0.4", "wrangler": "^3.0.0" } @@ -123,6 +125,425 @@ "integrity": "sha512-gQczWuGE2rxmpzOCNn0zLbx8Xz0gqspdE9S7tu4Xax39q1csgO/E9flcS+KG3GHB522ugOh84inmABDhpeJnvQ==", "dev": true }, + "node_modules/@esbuild-kit/cjs-loader": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/cjs-loader/-/cjs-loader-2.4.2.tgz", + "integrity": "sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==", + "dev": true, + "dependencies": { + "@esbuild-kit/core-utils": "^3.0.0", + "get-tsconfig": "^4.4.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.2.2.tgz", + "integrity": "sha512-Ub6LaRaAgF80dTSzUdXpFLM1pVDdmEVB9qb5iAzSpyDlX/mfJTFGOnZ516O05p5uWWteNviMKi4PAyEuRxI5gA==", + "dev": true, + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.5.5.tgz", + "integrity": "sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==", + "dev": true, + "dependencies": { + "@esbuild-kit/core-utils": "^3.0.0", + "get-tsconfig": "^4.4.0" + } + }, "node_modules/@esbuild-plugins/node-globals-polyfill": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.1.1.tgz", @@ -659,6 +1080,12 @@ "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, + "node_modules/@types/node": { + "version": "20.5.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.8.tgz", + "integrity": "sha512-eajsR9aeljqNhK028VG0Wuw+OaY5LLxYmxeoXynIoE6jannr9/Ucd1LL0hSSoafk5LTYG+FfqsyGt81Q6Zkybw==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz", @@ -1854,6 +2281,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.0.tgz", + "integrity": "sha512-pmjiZ7xtB8URYm74PlGJozDNyhvsVLUcpBa8DZBG3bWHwaHa9bPiRpiSfovw+fjhwONSCWKRyk+JQHEGZmMrzw==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -2809,6 +3248,15 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3335,6 +3783,23 @@ "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", "dev": true }, + "node_modules/tsx": { + "version": "3.12.8", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-3.12.8.tgz", + "integrity": "sha512-Lt9KYaRGF023tlLInPj8rgHwsZU8qWLBj4iRXNWxTfjIkU7canGL806AqKear1j722plHuiYNcL2ZCo6uS9UJA==", + "dev": true, + "dependencies": { + "@esbuild-kit/cjs-loader": "^2.4.2", + "@esbuild-kit/core-utils": "^3.2.2", + "@esbuild-kit/esm-loader": "^2.5.5" + }, + "bin": { + "tsx": "dist/cli.js" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 2a7c2c6..1f3199c 100644 --- a/package.json +++ b/package.json @@ -2,22 +2,25 @@ "name": "node-with-r2-poc", "version": "0.0.0", "private": true, + "type": "module", "scripts": { - "deploy:staging": "wrangler deploy -e staging", - "deploy:prod": "wrangler deploy -e prod", - "start": "wrangler dev", + "start": "wrangler dev --remote", "format": "prettier -u --write \"**/*.{ts,js,json}\"", "check-format": "prettier -u --check \"**/*.{ts,js,json}\"", - "lint": "eslint ./src" + "lint": "eslint ./src", + "test:unit": "node --loader tsx ./tests/unit/util.test.ts", + "test:e2e": "wrangler deploy --dry-run --outdir=dist && node --loader tsx ./tests/e2e/index.test.ts" }, "devDependencies": { "@cloudflare/workers-types": "^4.20230419.0", + "@types/node": "^20.5.8", "@typescript-eslint/eslint-plugin": "^6.5.0", "@typescript-eslint/parser": "^6.5.0", "eslint": "^8.48.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "prettier": "^3.0.1", + "tsx": "^3.12.8", "typescript": "^5.0.4", "wrangler": "^3.0.0" } diff --git a/src/handlers/directory.ts b/src/handlers/directory.ts index 4e0b78a..9411ce0 100644 --- a/src/handlers/directory.ts +++ b/src/handlers/directory.ts @@ -118,7 +118,7 @@ function renderDirectoryListing(

Index of ${url.pathname}

- ${tableElements} + ${tableElements.join('\n')}
FilenameModifiedSize
`, diff --git a/src/handlers/file.ts b/src/handlers/file.ts index 8d08ec0..f7ebc5c 100644 --- a/src/handlers/file.ts +++ b/src/handlers/file.ts @@ -23,6 +23,17 @@ function getStatusCode(request: Request, objectHasBody: boolean): number { // We have the full object body return 200; } + + if ( + request.headers.has('if-modified') || + request.headers.has('if-unmodified-since') || + request.headers.has('if-match') || + request.headers.has('if-none-match') + ) { + // No body due to precondition failure + return 412; + } + // We weren't given a body return 304; } @@ -42,10 +53,15 @@ export default async ( ): Promise => { let file: R2Object | null = null; if (request.method === 'GET') { - file = await env.R2_BUCKET.get(bucketPath, { - onlyIf: request.headers, - range: request.headers, - }); + try { + file = await env.R2_BUCKET.get(bucketPath, { + onlyIf: request.headers, + range: request.headers, + }); + } catch (e) { + // Unquoted etags make R2 api throw an error + return responses.BAD_REQUEST; + } } else if (request.method === 'HEAD') { file = await env.R2_BUCKET.head(bucketPath); } else { @@ -65,22 +81,22 @@ export default async ( status: getStatusCode(request, hasBody), headers: { etag: file.httpEtag, - 'Accept-Range': 'bytes', + 'accept-range': 'bytes', - 'Access-Control-Allow-Origin': url.pathname.endsWith('.json') + 'access-control-allow-origin': url.pathname.endsWith('.json') ? '*' : '', - 'Cache-Control': cacheControl, + 'cache-control': cacheControl, expires: file.httpMetadata?.cacheExpiry?.toUTCString() ?? '', - 'Last-Modified': file.uploaded.toUTCString(), - 'Content-Encoding': file.httpMetadata?.contentEncoding ?? '', - 'Content-Type': + 'last-modified': file.uploaded.toUTCString(), + 'content-encoding': file.httpMetadata?.contentEncoding ?? '', + 'content-type': file.httpMetadata?.contentType ?? 'application/octet-stream', - 'Content-Language': file.httpMetadata?.contentLanguage ?? '', - 'Content-Disposition': file.httpMetadata?.contentDisposition ?? '', - 'Content-Length': file.size.toString(), + 'content-language': file.httpMetadata?.contentLanguage ?? '', + 'content-disposition': file.httpMetadata?.contentDisposition ?? '', + 'content-length': file.size.toString(), }, } ); diff --git a/src/responses.ts b/src/responses.ts index cb60492..090472d 100644 --- a/src/responses.ts +++ b/src/responses.ts @@ -8,6 +8,9 @@ export default { Allow: 'GET, HEAD, OPTIONS', }, }), + BAD_REQUEST: new Response(undefined, { + status: 400, + }), FILE_NOT_FOUND: (request: Request): Response => { return new Response( request.method !== 'HEAD' ? 'File not found' : undefined, diff --git a/src/util.ts b/src/util.ts index af62aa4..ad9531a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -22,7 +22,10 @@ export function isCacheEnabled(env: Env): boolean { * @returns Mapped path if the resource is accessible, undefined * if the eyeball should not be trying to access the resource */ -export function mapUrlPathToBucketPath(url: URL, env: Env): string | undefined { +export function mapUrlPathToBucketPath( + url: URL, + env: Pick +): string | undefined { const urlToBucketPathMap = { dist: `nodejs/release${url.pathname.substring(5)}`, download: `nodejs${url.pathname.substring(9)}`, diff --git a/src/worker.ts b/src/worker.ts index 803728e..90d7143 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -60,7 +60,7 @@ async function getHandler( try { url = new URL(request.url); } catch (e) { - return new Response(undefined, { status: 400 }); + return responses.BAD_REQUEST; } const bucketPath = mapUrlPathToBucketPath(url, env); @@ -86,7 +86,7 @@ async function getHandler( await fileHandler(url, request, bucketPath, env); // Cache response if cache is enabled - if (shouldServeCache && response.status !== 304) { + if (shouldServeCache && response.status !== 304 && response.status !== 206) { ctx.waitUntil(cache.put(request, response.clone())); } diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..7f17be0 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,2 @@ +# Tests +Tests use Node's builtin test runner. See [https://nodejs.org/api/test.html](https://nodejs.org/api/test.html) for documentation. diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 0000000..441c46e --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,18 @@ +# End-to-End Tests +These are for testing the entire worker from client to server. +The [Miniflare 3 API](https://latest.miniflare.dev) is used to spin up a local +[Workerd](https://github.com/cloudflare/workerd) instance that the tests interact with. + +
+ +Test file names correspond to what's being tested. For example, [./directory.test.ts](./directory.test.ts) +contains tests related to directory listing or directories as a whole. + +## Adding a New Test File +Create the file and name it to something that's fitting to what it will be testing. +For simplicity sakes, take a look at the already existing test files and copy the structure. +Make sure to import the new test file into [./index.test.ts](./index.test.ts). + +## Adding a New Test +Each E2E test has one big `describe` call that contains all of its tests, add tests in that. +We usually prefer the `it` alias for defining tests. diff --git a/tests/e2e/directory.test.ts b/tests/e2e/directory.test.ts new file mode 100644 index 0000000..7af3797 --- /dev/null +++ b/tests/e2e/directory.test.ts @@ -0,0 +1,112 @@ +import { after, before, describe, it } from 'node:test'; +import assert from 'node:assert'; +import { readFile } from 'node:fs/promises'; +import { Miniflare } from 'miniflare'; + +describe('Directory Tests (Restricted Directory Listing)', () => { + let mf: Miniflare; + let url: URL; + before(async () => { + // Setup miniflare + mf = new Miniflare({ + scriptPath: './dist/worker.js', + modules: true, + bindings: { + DIRECTORY_LISTING: 'restricted', + CACHE_CONTROL: 'no-store', + DIRECTORY_CACHE_CONTROL: 'no-store', + }, + r2Persist: './tests/e2e/test-data', + r2Buckets: ['R2_BUCKET'], + }); + + // Wait for it Miniflare to start + url = await mf.ready; + }); + + it('allows `/dist` and returns expected html', async () => { + const [res, expectedHtml] = await Promise.all([ + mf.dispatchFetch(`${url}dist`), + readFile('./tests/e2e/test-data/expected-html/dist.html', { + encoding: 'utf-8', + }), + ]); + + assert.strictEqual(res.status, 200); + + // Assert that the html matches what we're expecting + // to be returned. If this passes, we can assume + // it'll pass for the other listings and therefore + // don't need to test it over and over again + const body = await res.text(); + assert.strictEqual(body, expectedHtml.replaceAll('\r', '')); + }); + + it('allows `/dist/`', async () => { + const res = await mf.dispatchFetch(`${url}dist/`); + + assert.strictEqual(res.status, 200); + }); + + it('allows `/download`', async () => { + const res = await mf.dispatchFetch(`${url}download`); + assert.strictEqual(res.status, 200); + }); + + it('allows `/download/`', async () => { + const res = await mf.dispatchFetch(`${url}download/`); + assert.strictEqual(res.status, 200); + }); + + it('allows `/docs`', async () => { + const res = await mf.dispatchFetch(`${url}docs`); + assert.strictEqual(res.status, 200); + }); + + it('allows `/docs/`', async () => { + const res = await mf.dispatchFetch(`${url}docs/`); + assert.strictEqual(res.status, 200); + }); + + it('allows `/api`', async () => { + const res = await mf.dispatchFetch(`${url}api`); + assert.strictEqual(res.status, 200); + }); + + it('allows `/api/`', async () => { + const res = await mf.dispatchFetch(`${url}api/`); + assert.strictEqual(res.status, 200); + }); + + it('allows `/metrics`', async () => { + const res = await mf.dispatchFetch(`${url}metrics`); + assert.strictEqual(res.status, 200); + }); + + it('allows `/metrics/`', async () => { + const res = await mf.dispatchFetch(`${url}metrics/`); + assert.strictEqual(res.status, 200); + }); + + it('returns 401 for unrecognized base paths', async () => { + let res = await mf.dispatchFetch(url); + assert.strictEqual(res.status, 401); + + res = await mf.dispatchFetch(`${url}/asd`); + assert.strictEqual(res.status, 401); + + res = await mf.dispatchFetch(`${url}/asd/123/`); + assert.strictEqual(res.status, 401); + }); + + it('returns 404 for unknown directory', async () => { + const res = await mf.dispatchFetch(`${url}/dist/asd123`); + assert.strictEqual(res.status, 404); + + const body = await res.text(); + assert.strict(body, 'Directory not found'); + }); + + // Cleanup Miniflare + after(async () => mf.dispose()); +}); diff --git a/tests/e2e/file.test.ts b/tests/e2e/file.test.ts new file mode 100644 index 0000000..2d815f3 --- /dev/null +++ b/tests/e2e/file.test.ts @@ -0,0 +1,149 @@ +import { after, before, describe, it } from 'node:test'; +import assert from 'node:assert'; +import { Miniflare } from 'miniflare'; +import path from 'node:path'; + +describe('File Tests', () => { + let mf: Miniflare; + let url: URL; + const cacheControl = 'no-store'; + before(async () => { + // Setup miniflare + mf = new Miniflare({ + scriptPath: './dist/worker.js', + modules: true, + bindings: { + DIRECTORY_LISTING: 'restricted', + CACHE_CONTROL: cacheControl, + DIRECTORY_CACHE_CONTROL: 'no-store', + }, + r2Persist: './tests/e2e/test-data', + r2Buckets: ['R2_BUCKET'], + }); + + // Wait for it Miniflare to start + url = await mf.ready; + }); +console.log('------------------------------------------------------------------------') +console.log(path.resolve('./tests/e2e/test-data')); +console.log('------------------------------------------------------------------------') + it('`/dist/index.json` returns expected body, status code, and headers', async () => { + const res = await mf.dispatchFetch(`${url}dist/index.json`); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.headers.get('content-type'), 'application/json'); + assert.strictEqual(res.headers.get('cache-control'), cacheControl); + assert.strictEqual(res.headers.has('etag'), true); + assert.strictEqual(res.headers.has('last-modified'), true); + assert.strictEqual(res.headers.has('content-type'), true); + + const body = await res.text(); + assert.strictEqual(body, `{ hello: 'world' }`); + }); + + it('returns 404 for unknown file', async () => { + const res = await mf.dispatchFetch(`${url}dist/asd123.json`); + assert.strictEqual(res.status, 404); + + const body = await res.text(); + assert.strictEqual(body, 'File not found'); + }); + + /** + * R2 supports all conditional headers except If-Range + * @see https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#conditional-operations + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests#conditional_headers + */ + it('handles if-modified-since correctly', async () => { + let res = await mf.dispatchFetch(`${url}dist/index.json`); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.headers.has('last-modified'), true); + + // Make sure it returns a 304 when If-Modified-Since + // >= file last modified + res = await mf.dispatchFetch(`${url}dist/index.json`, { + headers: { + 'if-modified-since': res.headers.get('last-modified')!, + }, + }); + assert.strictEqual(res.status, 304); + + // Make sure it returns a 200 w/ the file contents + // when If-Modified-Since < file last modified + res = await mf.dispatchFetch(`${url}dist/index.json`, { + headers: { + 'if-modified-since': new Date(0).toUTCString(), + }, + }); + assert.strictEqual(res.status, 200); + }); + + it('handles if-unmodified-since header correctly', async () => { + let res = await mf.dispatchFetch(`${url}dist/index.json`, { + headers: { + 'if-unmodified-since': new Date(0).toUTCString(), + }, + }); + assert.strictEqual(res.status, 412); + }); + + it('handles if-match correctly', async () => { + let res = await mf.dispatchFetch(`${url}dist/index.json`); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.headers.has('etag'), true); + + const originalETag = res.headers.get('etag')!; + + res = await mf.dispatchFetch(`${url}dist/index.json`, { + headers: { + 'if-match': '"asd"', + }, + }); + assert.strictEqual(res.status, 412); + + // If-Match w/ valid etag returns 200 + res = await mf.dispatchFetch(`${url}dist/index.json`, { + headers: { + 'if-match': originalETag, + }, + }); + assert.strictEqual(res.status, 200); + }); + + it('handles if-none-match correctly', async () => { + let res = await mf.dispatchFetch(`${url}dist/index.json`); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.headers.has('etag'), true); + + const originalETag = res.headers.get('etag')!; + + res = await mf.dispatchFetch(`${url}dist/index.json`, { + headers: { + 'if-none-match': '"asd"', + }, + }); + assert.strictEqual(res.status, 200); + + // If-None-Match w/ valid etag returns 412 + res = await mf.dispatchFetch(`${url}dist/index.json`, { + headers: { + 'if-none-match': originalETag, + }, + }); + assert.strictEqual(res.status, 412); + }); + + it('handles range header correctly', async () => { + const res = await mf.dispatchFetch(`${url}dist/index.json`, { + headers: { + range: 'bytes=0-7', + }, + }); + assert.strictEqual(res.status, 206); + + const body = await res.text(); + assert.strictEqual(body, '{ hello:'); + }); + + // Cleanup Miniflare + after(async () => mf.dispose()); +}); diff --git a/tests/e2e/index.test.ts b/tests/e2e/index.test.ts new file mode 100644 index 0000000..316e563 --- /dev/null +++ b/tests/e2e/index.test.ts @@ -0,0 +1,2 @@ +import './directory.test'; +import './file.test'; diff --git a/tests/e2e/test-data/R2_BUCKET/README.md b/tests/e2e/test-data/R2_BUCKET/README.md new file mode 100644 index 0000000..3f7dbe1 --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/README.md @@ -0,0 +1,2 @@ +# R2_BUCKET +This is a local mock of a R2 bucket with the dist folder's contents. diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/1e4de7990b4ef01257964a2f7d12b8551db3af2fd134f82c2b3f872824a9aac500000cea511db274 b/tests/e2e/test-data/R2_BUCKET/blobs/1e4de7990b4ef01257964a2f7d12b8551db3af2fd134f82c2b3f872824a9aac500000cea511db274 new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/1e4de7990b4ef01257964a2f7d12b8551db3af2fd134f82c2b3f872824a9aac500000cea511db274 @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/2b8de93bc9a59f7b74004036973e87ede6a8985145b5b10c87db0b71310736d500000cea51c8d30c b/tests/e2e/test-data/R2_BUCKET/blobs/2b8de93bc9a59f7b74004036973e87ede6a8985145b5b10c87db0b71310736d500000cea51c8d30c new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/2b8de93bc9a59f7b74004036973e87ede6a8985145b5b10c87db0b71310736d500000cea51c8d30c @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/452c0bddf19b5647831280c01a090c815526157e8eb6cc71b76f951576ddcfff00000cea50e229d4 b/tests/e2e/test-data/R2_BUCKET/blobs/452c0bddf19b5647831280c01a090c815526157e8eb6cc71b76f951576ddcfff00000cea50e229d4 new file mode 100644 index 0000000..ee64844 --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/452c0bddf19b5647831280c01a090c815526157e8eb6cc71b76f951576ddcfff00000cea50e229d4 @@ -0,0 +1 @@ +123asd \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/755deadb8510a4bcf8b632106ce3bb2884cbb19628ff06a4b2c4cd7a15e9711800000cea51dc0fa8 b/tests/e2e/test-data/R2_BUCKET/blobs/755deadb8510a4bcf8b632106ce3bb2884cbb19628ff06a4b2c4cd7a15e9711800000cea51dc0fa8 new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/755deadb8510a4bcf8b632106ce3bb2884cbb19628ff06a4b2c4cd7a15e9711800000cea51dc0fa8 @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/7637f4431321361e34e8bdba83dfc0b7c4aad8c3619d895b7fcfac1cbea3bae700000cea51681f30 b/tests/e2e/test-data/R2_BUCKET/blobs/7637f4431321361e34e8bdba83dfc0b7c4aad8c3619d895b7fcfac1cbea3bae700000cea51681f30 new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/7637f4431321361e34e8bdba83dfc0b7c4aad8c3619d895b7fcfac1cbea3bae700000cea51681f30 @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/7cd7aaa22139ae89634e0a4f695745e7d195ce9c16d9c865684739f46029ba3900000cea5198fe20 b/tests/e2e/test-data/R2_BUCKET/blobs/7cd7aaa22139ae89634e0a4f695745e7d195ce9c16d9c865684739f46029ba3900000cea5198fe20 new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/7cd7aaa22139ae89634e0a4f695745e7d195ce9c16d9c865684739f46029ba3900000cea5198fe20 @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/84ec646e6ee82c6a2b627a3c7988a1e630e43c3b53644c20f30f90cc1bdc4ff300000cea51b2f884 b/tests/e2e/test-data/R2_BUCKET/blobs/84ec646e6ee82c6a2b627a3c7988a1e630e43c3b53644c20f30f90cc1bdc4ff300000cea51b2f884 new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/84ec646e6ee82c6a2b627a3c7988a1e630e43c3b53644c20f30f90cc1bdc4ff300000cea51b2f884 @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/8dc8517f83d7d2ac8a3030da0eedd2c4956c4e3e0391bd3b94cc0701c7ddb54100000cea520b6ca8 b/tests/e2e/test-data/R2_BUCKET/blobs/8dc8517f83d7d2ac8a3030da0eedd2c4956c4e3e0391bd3b94cc0701c7ddb54100000cea520b6ca8 new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/8dc8517f83d7d2ac8a3030da0eedd2c4956c4e3e0391bd3b94cc0701c7ddb54100000cea520b6ca8 @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/9c101170154bc7872e5b93675a74706d03205b791f115401e3b7d2a2a4cd597c00000cea51381330 b/tests/e2e/test-data/R2_BUCKET/blobs/9c101170154bc7872e5b93675a74706d03205b791f115401e3b7d2a2a4cd597c00000cea51381330 new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/9c101170154bc7872e5b93675a74706d03205b791f115401e3b7d2a2a4cd597c00000cea51381330 @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/9fbf67cc8db78f5ca67b84b435be22b5a814b29ff344e19a7e4da43500357d7e00000cea5181ac34 b/tests/e2e/test-data/R2_BUCKET/blobs/9fbf67cc8db78f5ca67b84b435be22b5a814b29ff344e19a7e4da43500357d7e00000cea5181ac34 new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/9fbf67cc8db78f5ca67b84b435be22b5a814b29ff344e19a7e4da43500357d7e00000cea5181ac34 @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/b11cbe7ea145ec48630b0d87fbd25b304348a215548c998c4798e73ce8dca44e00000cea51509568 b/tests/e2e/test-data/R2_BUCKET/blobs/b11cbe7ea145ec48630b0d87fbd25b304348a215548c998c4798e73ce8dca44e00000cea51509568 new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/b11cbe7ea145ec48630b0d87fbd25b304348a215548c998c4798e73ce8dca44e00000cea51509568 @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/b2b1ba500a2c700706ab817da2f848631db25d34190b88c7c7cb7dd3d63364ca00000cea5084db80 b/tests/e2e/test-data/R2_BUCKET/blobs/b2b1ba500a2c700706ab817da2f848631db25d34190b88c7c7cb7dd3d63364ca00000cea5084db80 new file mode 100644 index 0000000..1365cc7 --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/b2b1ba500a2c700706ab817da2f848631db25d34190b88c7c7cb7dd3d63364ca00000cea5084db80 @@ -0,0 +1 @@ +{ hello: 'world' } \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/b9e14d8b54ea621f6549fee94f557cdb43646966979dcf3ac380ba0273de9de400000cea50ffe460 b/tests/e2e/test-data/R2_BUCKET/blobs/b9e14d8b54ea621f6549fee94f557cdb43646966979dcf3ac380ba0273de9de400000cea50ffe460 new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/b9e14d8b54ea621f6549fee94f557cdb43646966979dcf3ac380ba0273de9de400000cea50ffe460 @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/e62d82792f96282b1c49fb7cdc70824ff825620ac2fd80566a62d51df363c3a700000cea51ef17ec b/tests/e2e/test-data/R2_BUCKET/blobs/e62d82792f96282b1c49fb7cdc70824ff825620ac2fd80566a62d51df363c3a700000cea51ef17ec new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/e62d82792f96282b1c49fb7cdc70824ff825620ac2fd80566a62d51df363c3a700000cea51ef17ec @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/blobs/f34b8641aebf56ffe850eaf5d26f9604418e4d48de4766cd903ce22f7485848900000cea50b18770 b/tests/e2e/test-data/R2_BUCKET/blobs/f34b8641aebf56ffe850eaf5d26f9604418e4d48de4766cd903ce22f7485848900000cea50b18770 new file mode 100644 index 0000000..f7ab40c --- /dev/null +++ b/tests/e2e/test-data/R2_BUCKET/blobs/f34b8641aebf56ffe850eaf5d26f9604418e4d48de4766cd903ce22f7485848900000cea50b18770 @@ -0,0 +1 @@ +asd123 \ No newline at end of file diff --git a/tests/e2e/test-data/R2_BUCKET/db.sqlite b/tests/e2e/test-data/R2_BUCKET/db.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..5784792c81f94ed11e7fd575cc5e4ffd62742193 GIT binary patch literal 28672 zcmeI3TW?%f701V!#3`O;Ob{Xzs2CCn64504eo2CYX(z#rof5~Dh$3Y6HF0WtjAz^? zEd@fkNC*k_TkwG3GxU`wkoW|=Ao0utkNl547iT=N26$?;Pc`Yx+54Qc)^Dx#Uwif> zcke8%_fji&b~atF#cQ9f9XnS0cB@sZ)sEqH60gBg$D8AW5BUGgf%ngQd#rZp(O*u@ z|GPHZSgbXFHUG(}tIey8#h3r}ybH?tLZA>R1PXydpb#ho3V}l4Q$gUP^CwQ8;e6(o z@AaJDNNbyUZF6^{x4!LmdTZ4m-3g~=Utei2thQUL3*T94w?ryGaMmcG`;~%BaTYD!x*&iKq zXn(kS9^)5UZ&c2%?2%f$y|}u#u(Wiqf3p3?{^P#4vbemka<6r(eXlk0{k7Hsw=c|H zXwS}`xyNT}>sv8B?*3>4Ux3!!Zm+ZVd2K=z*64(jC!c;;KY2#$nU7wX5b=Hhp1rPr z^7s)^?@yT=_XFbbhA#DAD zKCB^(LzoWc^gf|?(@uB2vo(p$|B&VTZlw5};usV`chckjzgW#(kjG}vES@_m3ImdR z?E4?8@A5BIx_Sz)`QyvAm;ZF?$IUnAztZ@}-2KLvKJ)SHZ(sWJ$=feAjxV41>xt9V z%yJY0g+L*21c9duR~px!UR<78K5_c=>8(yokGhxNH5Y^1ayw5h=k<+rsrR_2rHsi% zZzLs7_>hfPoKd1e;@&f3MDU(k#Z1mbVFn8#MD3_dR#R$7b%f+3je%rmSfvNQrS(*%!US{Tfrpaq1mCYt6J@cDi|8w)FAADH-_w-xW8hYph zI^@oJ=w9CFJm?PXH!;9;oehsVG8c^Fgp=qKPq*ydeTb3)l%u`sK zy$&&GVwlJogcKt#upAAMN;zBr&Y3G7xYI+^Y06Zmkfkxis>~%(ndYP%mxfYBos1D5 zr!)O$ebcY2#?_(e{q)&dfAFxkaUglBOmL1d%B@SrDlQVTC@U+qkV!{sWk@zq6>TsG zu@RcvEEHkZJ8nlFScIMt$+=dJil9=C$t2E$5JnNJ;hS@2IuAbRdvL`x=(qEDUgmq2Q76nLZdiin zF=cOP$cksAl0;rn=j?RG|D_b1ZlZk*IY#2^1qzoN%e5P9rxA zb%Bd$pf8ykl(J+TcNA3t8HQqjScxg!*i#?q&rR95HSR|2R2qZ@IfCJ~*M|Yoit8){ zr;tw$aHs|2qjylI96&w@=b{N1ve8)Sbq?7D8hmoxyL6zOm7z#47|)QR93k~22Pxr( zP0Udy7)4!A7m)V{`NR+B)1D^-HS)AFMJJS>OaY>+;ItER}EmcriTBM^1jIlhO`A_=hzbP7b#sd)^x_5S5 z=-o3uKN zoZ*IYjvgQk3R}x80>xEmC)7jWtRoS^5Tp zmZ7Yue~D(`xy$GelnbH=fZ&^$4sV$feujPE3J(Zy$2D2;04M(M_XCn#J{!g>uMw9rb0XbEZsd5<7SBh0Ax5l@ZQ zL>R8Elqf2g?xI#vB||lbMrdfqlnbYK``)}o8u$MjZ!WoR5RyTsuvEZ`M=DXRgBTKq zP)E@B3`(CVDmD5tI$F>ai?(bf&ZvlZVR<-=5B#)IM68~2)z}l{n4$uyhFSzj1G5UN z%-KX4kl`5)F;$jF3EhZ-|Kti=x{lEXRw?d#Ak1_=+dKZXUk(5<#9LS z7*1)eK^$H` z!^X$u%J8#jIdk*BtIap(YRw-uZ_GcOzcK&S`Pt^*o1Zjyn;$pl=KnDNx90Dg=Z{$P zm!U$S5GVu+fkL1VCR1PXyq8G%>p*Jt{B?uWnQ(gUN1f6%$|N?p%P{Hf&H z=j#`TBZF!PzV$Pg+L)t2owT^Kq0XI@Bhp9|B?xXKp{{F6as}nAy5bu0);>! uPzV$Pg}{qVp#1*-V#l|5R|pgWg+L)t2owT^Kp{{F6as}nAy5cZ1pWh7T(?R9 literal 0 HcmV?d00001 diff --git a/tests/e2e/test-data/expected-html/README.md b/tests/e2e/test-data/expected-html/README.md new file mode 100644 index 0000000..6c15655 --- /dev/null +++ b/tests/e2e/test-data/expected-html/README.md @@ -0,0 +1,2 @@ +# expected-html +Expected html for listing responses. These should be 1:1 to what the worker returns diff --git a/tests/e2e/test-data/expected-html/dist.html b/tests/e2e/test-data/expected-html/dist.html new file mode 100644 index 0000000..518ace1 --- /dev/null +++ b/tests/e2e/test-data/expected-html/dist.html @@ -0,0 +1,47 @@ + + + + Index of /dist + + + + + +

Index of /dist

+ + + + + + + + + + + + + + + + + +
FilenameModifiedSize
../--
latest/--
index.json2023-09-12 05:43Z18 B
+ + \ No newline at end of file diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..df890c6 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["node"] + } +} diff --git a/tests/unit/README.md b/tests/unit/README.md new file mode 100644 index 0000000..614831f --- /dev/null +++ b/tests/unit/README.md @@ -0,0 +1,22 @@ +# Unit Tests +These are for testing specific functions in the worker. + +
+ +Test file names correspond to the file name in the [`src`](../../src) directory that is being tested. +For example, [`./util.test.ts`](./util.test.ts) tests functions defined from [`src/util.ts`](../../src/util.ts). + +## Adding a New Test File +Create the file and name it the same name as the file in the [`src`](../../src) directory. +See [Adding a New Test](#adding-a-new-test) for testing the functions. +Make sure to import the new test file into [./index.test.ts](./index.test.ts). + +## Adding a New Test +Each function has its tests wrapped in a `describe` call just for neatness. +We usually prefer the `it` alias for defining tests. For example, tests for the +function `doSomething` would look something like this: +```js +describe('doSomething', () => { + it('does something', () => {/*...*/}); +}); +``` diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts new file mode 100644 index 0000000..c44bb5f --- /dev/null +++ b/tests/unit/index.test.ts @@ -0,0 +1 @@ +import './util.test'; diff --git a/tests/unit/util.test.ts b/tests/unit/util.test.ts new file mode 100644 index 0000000..90d0144 --- /dev/null +++ b/tests/unit/util.test.ts @@ -0,0 +1,126 @@ +import assert from 'node:assert'; +import { describe, it } from 'node:test'; +import { + isDirectoryPath, + mapUrlPathToBucketPath, + niceBytes, +} from '../../src/util'; + +describe('mapUrlPathToBucketPath', () => { + it('converts `/unknown-base-path` to undefined when DIRECTORY_LISTING=restricted', () => { + const result = mapUrlPathToBucketPath( + new URL('http://localhost/unknown-base-path'), + { + DIRECTORY_LISTING: 'restricted', + } + ); + assert.strictEqual(result, undefined); + }); + + it('converts `/unknown-base-path` to `unknown-base-path` when DIRECTORY_LISTING=on', () => { + const result = mapUrlPathToBucketPath( + new URL('http://localhost/unknown-base-path'), + { + DIRECTORY_LISTING: 'on', + } + ); + assert.strictEqual(result, 'unknown-base-path'); + }); + + it('converts `/dist` to `nodejs/release`', () => { + const result = mapUrlPathToBucketPath(new URL('http://localhost/dist'), { + DIRECTORY_LISTING: 'restricted', + }); + assert.strictEqual(result, 'nodejs/release'); + }); + + it('converts `/dist/latest` to `nodejs/release/latest`', () => { + const result = mapUrlPathToBucketPath( + new URL('http://localhost/dist/latest'), + { + DIRECTORY_LISTING: 'restricted', + } + ); + assert.strictEqual(result, 'nodejs/release/latest'); + }); + + it('converts `/download` to `nodejs`', () => { + const result = mapUrlPathToBucketPath( + new URL('http://localhost/download'), + { + DIRECTORY_LISTING: 'restricted', + } + ); + assert.strictEqual(result, 'nodejs'); + }); + + it('converts `/download/releases` to `nodejs/releases`', () => { + const result = mapUrlPathToBucketPath( + new URL('http://localhost/download/releases'), + { + DIRECTORY_LISTING: 'restricted', + } + ); + assert.strictEqual(result, 'nodejs/releases'); + }); + + it('converts `/docs` to `nodejs/docs`', () => { + const result = mapUrlPathToBucketPath(new URL('http://localhost/docs'), { + DIRECTORY_LISTING: 'restricted', + }); + assert.strictEqual(result, 'nodejs/docs'); + }); + + it('converts `/docs/releases` to `nodejs/docs/latest`', () => { + const result = mapUrlPathToBucketPath( + new URL('http://localhost/docs/latest'), + { + DIRECTORY_LISTING: 'restricted', + } + ); + assert.strictEqual(result, 'nodejs/docs/latest'); + }); + + it('converts `/api` to `nodejs/docs`', () => { + const result = mapUrlPathToBucketPath(new URL('http://localhost/api'), { + DIRECTORY_LISTING: 'restricted', + }); + assert.strictEqual(result, 'nodejs/docs/latest/api'); + }); + + it('converts `/api/assert.html` to `nodejs/docs/latest/api/assert.html`', () => { + const result = mapUrlPathToBucketPath( + new URL('http://localhost/api/assert.html'), + { + DIRECTORY_LISTING: 'restricted', + } + ); + assert.strictEqual(result, 'nodejs/docs/latest/api/assert.html'); + }); +}); + +describe('isDirectoryPath', () => { + it('returns true for `/dist/`', () => { + assert.strictEqual(isDirectoryPath('/dist/'), true); + }); + + it('returns true for `/dist`', () => { + assert.strictEqual(isDirectoryPath('/dist'), true); + }); + + it('returns false for `/dist/index.json`', () => { + assert.strictEqual(isDirectoryPath('/dist/index.json'), false); + }); +}); + +describe('niceBytes', () => { + it('converts 10 to `10 B`', () => { + const result = niceBytes(10); + assert.strictEqual(result, '10 B'); + }); + + it('converts 1024 to `1.0 KB`', () => { + const result = niceBytes(1024); + assert.strictEqual(result, '1.0 KB'); + }); +});