diff --git a/package.json b/package.json index ff4a662..453aa78 100644 --- a/package.json +++ b/package.json @@ -7,17 +7,15 @@ "scripts": { "dev": "wrangler dev src/index.ts --port 8080", "deploy": "wrangler deploy --minify src/index.ts", - "env:generate": "tsx src/scripts/generateEnv.ts", - "env:verify": "tsx src/scripts/verifyEnv.ts", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "test": "vitest", - "coverage": "vitest run --coverage", + "test:ui": "vitest --ui", + "coverage": "vitest --coverage", "prepare": "husky" }, "dependencies": { "@consumet/extensions": "github:consumet/consumet.ts#3dd0ccb", - "@haverstack/axios-fetch-adapter": "^0.12.0", "@hono/swagger-ui": "^0.5.1", "@hono/zod-openapi": "^0.19.5", "@hono/zod-validator": "^0.2.2", @@ -39,16 +37,16 @@ "devDependencies": { "@0no-co/graphqlsp": "^1.12.16", "@cloudflare/vitest-pool-workers": "^0.10.7", - "@cloudflare/workers-types": "^4.20250423.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/lodash.isequal": "^4.5.8", "@types/lodash.mapkeys": "^4.6.9", "@types/luxon": "^3.6.2", "@types/node": "^22.10.1", "@types/pngjs": "^6.0.5", - "@vitest/coverage-v8": "^3.2.4", + "@vitest/coverage-istanbul": "3.2.4", "@vitest/runner": "^3.2.4", "@vitest/snapshot": "^3.2.4", + "@vitest/ui": "^3.2.4", "cloudflare": "^5.2.0", "dotenv": "^17.2.3", "drizzle-kit": "^0.31.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20259df..85fec75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,9 +15,6 @@ importers: "@consumet/extensions": specifier: github:consumet/consumet.ts#3dd0ccb version: https://codeload.github.com/consumet/consumet.ts/tar.gz/3dd0ccb - "@haverstack/axios-fetch-adapter": - specifier: ^0.12.0 - version: 0.12.0(axios@0.27.2) "@hono/swagger-ui": specifier: ^0.5.1 version: 0.5.2(hono@4.10.4) @@ -32,7 +29,7 @@ importers: version: 2.0.5(patch_hash=814a3f2d2a39f286e4f86929789e0ada33593d88cf2fb1eb3cf2cc2425c7dfaf) drizzle-orm: specifier: ^0.44.7 - version: 0.44.7(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.4)(bun-types@1.3.1(@types/react@19.2.2)) + version: 0.44.7(@libsql/client@0.15.4)(bun-types@1.3.1(@types/react@19.2.2)) gql.tada: specifier: ^1.8.10 version: 1.8.13(graphql@16.12.0)(typescript@5.9.3) @@ -75,10 +72,7 @@ importers: version: 1.15.0(graphql@16.12.0)(typescript@5.9.3) "@cloudflare/vitest-pool-workers": specifier: ^0.10.7 - version: 0.10.7(@cloudflare/workers-types@4.20251014.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) - "@cloudflare/workers-types": - specifier: ^4.20250423.0 - version: 4.20251014.0 + version: 0.10.7(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4) "@trivago/prettier-plugin-sort-imports": specifier: ^4.3.0 version: 4.3.0(prettier@3.6.2) @@ -97,15 +91,18 @@ importers: "@types/pngjs": specifier: ^6.0.5 version: 6.0.5 - "@vitest/coverage-v8": - specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + "@vitest/coverage-istanbul": + specifier: 3.2.4 + version: 3.2.4(vitest@3.2.4) "@vitest/runner": specifier: ^3.2.4 version: 3.2.4 "@vitest/snapshot": specifier: ^3.2.4 version: 3.2.4 + "@vitest/ui": + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4) cloudflare: specifier: ^5.2.0 version: 5.2.0 @@ -153,10 +150,10 @@ importers: version: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + version: 3.2.4(@types/node@22.18.13)(@vitest/ui@3.2.4)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) wrangler: specifier: ^4.51.0 - version: 4.51.0(@cloudflare/workers-types@4.20251014.0) + version: 4.51.0 zx: specifier: 8.1.5 version: 8.1.5 @@ -182,13 +179,6 @@ packages: graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 typescript: ^5.0.0 - "@ampproject/remapping@2.3.0": - resolution: - { - integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==, - } - engines: { node: ">=6.0.0" } - "@asteasolutions/zod-to-openapi@7.3.4": resolution: { @@ -204,6 +194,20 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/compat-data@7.28.5": + resolution: + { + integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==, + } + engines: { node: ">=6.9.0" } + + "@babel/core@7.28.5": + resolution: + { + integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==, + } + engines: { node: ">=6.9.0" } + "@babel/generator@7.17.7": resolution: { @@ -218,6 +222,13 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/helper-compilation-targets@7.27.2": + resolution: + { + integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==, + } + engines: { node: ">=6.9.0" } + "@babel/helper-environment-visitor@7.24.7": resolution: { @@ -232,6 +243,13 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/helper-globals@7.28.0": + resolution: + { + integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==, + } + engines: { node: ">=6.9.0" } + "@babel/helper-hoist-variables@7.24.7": resolution: { @@ -239,6 +257,22 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/helper-module-imports@7.27.1": + resolution: + { + integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-module-transforms@7.28.3": + resolution: + { + integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0 + "@babel/helper-split-export-declaration@7.24.7": resolution: { @@ -260,6 +294,20 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/helper-validator-option@7.27.1": + resolution: + { + integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==, + } + engines: { node: ">=6.9.0" } + + "@babel/helpers@7.28.4": + resolution: + { + integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==, + } + engines: { node: ">=6.9.0" } + "@babel/parser@7.28.5": resolution: { @@ -282,6 +330,13 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/traverse@7.28.5": + resolution: + { + integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==, + } + engines: { node: ">=6.9.0" } + "@babel/types@7.17.0": resolution: { @@ -296,13 +351,6 @@ packages: } engines: { node: ">=6.9.0" } - "@bcoe/v8-coverage@1.0.2": - resolution: - { - integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==, - } - engines: { node: ">=18" } - "@cloudflare/kv-asset-handler@0.4.0": resolution: { @@ -441,12 +489,6 @@ packages: cpu: [x64] os: [win32] - "@cloudflare/workers-types@4.20251014.0": - resolution: - { - integrity: sha512-tEW98J/kOa0TdylIUOrLKRdwkUw0rvvYVlo+Ce0mqRH3c8kSoxLzUH9gfCvwLe0M89z1RkzFovSKAW2Nwtyn3w==, - } - "@consumet/extensions@https://codeload.github.com/consumet/consumet.ts/tar.gz/3dd0ccb": resolution: { @@ -1496,14 +1538,6 @@ packages: } engines: { node: ">=18.0.0" } - "@haverstack/axios-fetch-adapter@0.12.0": - resolution: - { - integrity: sha512-+9WzqzeIvEC6Qrs6ImSqaX5P+eCrWbhzR+GoB7+p8/yqxmq59CPEo0uFuu0wINU9DQuJMzyER9WYdpYVwZV9rw==, - } - peerDependencies: - axios: ^0.21.1 - "@hono/swagger-ui@0.5.2": resolution: { @@ -1722,6 +1756,12 @@ packages: integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, } + "@jridgewell/remapping@2.3.5": + resolution: + { + integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, + } + "@jridgewell/resolve-uri@3.1.2": resolution: { @@ -1890,6 +1930,12 @@ packages: } engines: { node: ">=14" } + "@polka/url@1.0.0-next.29": + resolution: + { + integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==, + } + "@poppinss/colors@4.1.5": resolution: { @@ -2223,17 +2269,13 @@ packages: integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==, } - "@vitest/coverage-v8@3.2.4": + "@vitest/coverage-istanbul@3.2.4": resolution: { - integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==, + integrity: sha512-IDlpuFJiWU9rhcKLkpzj8mFu/lpe64gVgnV15ZOrYx1iFzxxrxCzbExiUEKtwwXRvEiEMUS6iZeYgnMxgbqbxQ==, } peerDependencies: - "@vitest/browser": 3.2.4 vitest: 3.2.4 - peerDependenciesMeta: - "@vitest/browser": - optional: true "@vitest/expect@3.2.4": resolution: @@ -2279,6 +2321,14 @@ packages: integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==, } + "@vitest/ui@3.2.4": + resolution: + { + integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==, + } + peerDependencies: + vitest: 3.2.4 + "@vitest/utils@3.2.4": resolution: { @@ -2419,12 +2469,6 @@ packages: } engines: { node: ">=12" } - ast-v8-to-istanbul@0.3.8: - resolution: - { - integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==, - } - asynckit@0.4.0: resolution: { @@ -2450,6 +2494,13 @@ packages: integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, } + baseline-browser-mapping@2.9.4: + resolution: + { + integrity: sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==, + } + hasBin: true + birpc@0.2.14: resolution: { @@ -2487,6 +2538,14 @@ packages: } engines: { node: ">=8" } + browserslist@4.28.1: + resolution: + { + integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==, + } + engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } + hasBin: true + buffer-equal-constant-time@1.0.1: resolution: { @@ -2535,6 +2594,12 @@ packages: } engines: { node: ">= 0.4" } + caniuse-lite@1.0.30001759: + resolution: + { + integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==, + } + chai@5.3.3: resolution: { @@ -2653,6 +2718,12 @@ packages: integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==, } + convert-source-map@2.0.0: + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } + cookie@1.0.2: resolution: { @@ -2912,6 +2983,12 @@ packages: integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==, } + electron-to-chromium@1.5.266: + resolution: + { + integrity: sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==, + } + emoji-regex@10.6.0: resolution: { @@ -3037,6 +3114,13 @@ packages: engines: { node: ">=18" } hasBin: true + escalade@3.2.0: + resolution: + { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, + } + engines: { node: ">=6" } + estree-walker@3.0.3: resolution: { @@ -3115,6 +3199,12 @@ packages: } engines: { node: ^12.20 || >= 14.13 } + fflate@0.8.2: + resolution: + { + integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==, + } + fill-range@7.1.1: resolution: { @@ -3122,6 +3212,12 @@ packages: } engines: { node: ">=8" } + flatted@3.3.3: + resolution: + { + integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==, + } + follow-redirects@1.15.11: resolution: { @@ -3203,6 +3299,13 @@ packages: } engines: { node: ">= 0.4" } + gensync@1.0.0-beta.2: + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: { node: ">=6.9.0" } + get-east-asian-width@1.4.0: resolution: { @@ -3521,6 +3624,13 @@ packages: } engines: { node: ">=8" } + istanbul-lib-instrument@6.0.3: + resolution: + { + integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==, + } + engines: { node: ">=10" } + istanbul-lib-report@3.0.1: resolution: { @@ -3600,6 +3710,14 @@ packages: engines: { node: ">=6" } hasBin: true + json5@2.2.3: + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: ">=6" } + hasBin: true + jwa@2.0.1: resolution: { @@ -3687,6 +3805,12 @@ packages: integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, } + lru-cache@5.1.1: + resolution: + { + integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, + } + luxon@3.7.2: resolution: { @@ -3814,6 +3938,13 @@ packages: engines: { node: ">=10" } hasBin: true + mrmime@2.0.1: + resolution: + { + integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==, + } + engines: { node: ">=10" } + ms@2.1.3: resolution: { @@ -3855,6 +3986,12 @@ packages: } engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + node-releases@2.0.27: + resolution: + { + integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==, + } + npm-run-path@5.3.0: resolution: { @@ -4095,6 +4232,13 @@ packages: integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, } + semver@6.3.1: + resolution: + { + integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, + } + hasBin: true + semver@7.7.3: resolution: { @@ -4150,6 +4294,13 @@ packages: integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==, } + sirv@3.0.2: + resolution: + { + integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==, + } + engines: { node: ">=18" } + slice-ansi@5.0.0: resolution: { @@ -4348,6 +4499,13 @@ packages: } engines: { node: ">=8.0" } + totalist@3.0.1: + resolution: + { + integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==, + } + engines: { node: ">=6" } + tr46@0.0.3: resolution: { @@ -4427,6 +4585,15 @@ packages: integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==, } + update-browserslist-db@1.2.2: + resolution: + { + integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==, + } + hasBin: true + peerDependencies: + browserslist: ">= 4.21.0" + urlpattern-polyfill@10.1.0: resolution: { @@ -4696,6 +4863,12 @@ packages: utf-8-validate: optional: true + yallist@3.1.1: + resolution: + { + integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, + } + yaml@2.8.1: resolution: { @@ -4747,11 +4920,6 @@ snapshots: graphql: 16.12.0 typescript: 5.9.3 - "@ampproject/remapping@2.3.0": - dependencies: - "@jridgewell/gen-mapping": 0.3.13 - "@jridgewell/trace-mapping": 0.3.31 - "@asteasolutions/zod-to-openapi@7.3.4(zod@3.25.76)": dependencies: openapi3-ts: 4.5.0 @@ -4763,6 +4931,28 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + "@babel/compat-data@7.28.5": {} + + "@babel/core@7.28.5": + dependencies: + "@babel/code-frame": 7.27.1 + "@babel/generator": 7.28.5 + "@babel/helper-compilation-targets": 7.27.2 + "@babel/helper-module-transforms": 7.28.3(@babel/core@7.28.5) + "@babel/helpers": 7.28.4 + "@babel/parser": 7.28.5 + "@babel/template": 7.27.2 + "@babel/traverse": 7.28.5 + "@babel/types": 7.28.5 + "@jridgewell/remapping": 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + "@babel/generator@7.17.7": dependencies: "@babel/types": 7.17.0 @@ -4777,6 +4967,14 @@ snapshots: "@jridgewell/trace-mapping": 0.3.31 jsesc: 3.1.0 + "@babel/helper-compilation-targets@7.27.2": + dependencies: + "@babel/compat-data": 7.28.5 + "@babel/helper-validator-option": 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + "@babel/helper-environment-visitor@7.24.7": dependencies: "@babel/types": 7.28.5 @@ -4786,10 +4984,28 @@ snapshots: "@babel/template": 7.27.2 "@babel/types": 7.28.5 + "@babel/helper-globals@7.28.0": {} + "@babel/helper-hoist-variables@7.24.7": dependencies: "@babel/types": 7.28.5 + "@babel/helper-module-imports@7.27.1": + dependencies: + "@babel/traverse": 7.28.5 + "@babel/types": 7.28.5 + transitivePeerDependencies: + - supports-color + + "@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)": + dependencies: + "@babel/core": 7.28.5 + "@babel/helper-module-imports": 7.27.1 + "@babel/helper-validator-identifier": 7.28.5 + "@babel/traverse": 7.28.5 + transitivePeerDependencies: + - supports-color + "@babel/helper-split-export-declaration@7.24.7": dependencies: "@babel/types": 7.28.5 @@ -4798,6 +5014,13 @@ snapshots: "@babel/helper-validator-identifier@7.28.5": {} + "@babel/helper-validator-option@7.27.1": {} + + "@babel/helpers@7.28.4": + dependencies: + "@babel/template": 7.27.2 + "@babel/types": 7.28.5 + "@babel/parser@7.28.5": dependencies: "@babel/types": 7.28.5 @@ -4823,6 +5046,18 @@ snapshots: transitivePeerDependencies: - supports-color + "@babel/traverse@7.28.5": + dependencies: + "@babel/code-frame": 7.27.1 + "@babel/generator": 7.28.5 + "@babel/helper-globals": 7.28.0 + "@babel/parser": 7.28.5 + "@babel/template": 7.27.2 + "@babel/types": 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + "@babel/types@7.17.0": dependencies: "@babel/helper-validator-identifier": 7.28.5 @@ -4833,8 +5068,6 @@ snapshots: "@babel/helper-string-parser": 7.27.1 "@babel/helper-validator-identifier": 7.28.5 - "@bcoe/v8-coverage@1.0.2": {} - "@cloudflare/kv-asset-handler@0.4.0": dependencies: mime: 3.0.0 @@ -4855,7 +5088,7 @@ snapshots: optionalDependencies: workerd: 1.20251125.0 - "@cloudflare/vitest-pool-workers@0.10.7(@cloudflare/workers-types@4.20251014.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))": + "@cloudflare/vitest-pool-workers@0.10.7(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4)": dependencies: "@vitest/runner": 3.2.4 "@vitest/snapshot": 3.2.4 @@ -4864,8 +5097,8 @@ snapshots: devalue: 5.5.0 miniflare: 4.20251109.1 semver: 7.7.3 - vitest: 3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) - wrangler: 4.48.0(@cloudflare/workers-types@4.20251014.0) + vitest: 3.2.4(@types/node@22.18.13)(@vitest/ui@3.2.4)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + wrangler: 4.48.0 zod: 3.25.76 transitivePeerDependencies: - "@cloudflare/workers-types" @@ -4902,8 +5135,6 @@ snapshots: "@cloudflare/workerd-windows-64@1.20251125.0": optional: true - "@cloudflare/workers-types@4.20251014.0": {} - "@consumet/extensions@https://codeload.github.com/consumet/consumet.ts/tar.gz/3dd0ccb": dependencies: ascii-url-encoder: 1.2.0 @@ -5316,10 +5547,6 @@ snapshots: "@repeaterjs/repeater": 3.0.6 tslib: 2.8.1 - "@haverstack/axios-fetch-adapter@0.12.0(axios@0.27.2)": - dependencies: - axios: 0.27.2 - "@hono/swagger-ui@0.5.2(hono@4.10.4)": dependencies: hono: 4.10.4 @@ -5433,6 +5660,11 @@ snapshots: "@jridgewell/sourcemap-codec": 1.5.5 "@jridgewell/trace-mapping": 0.3.31 + "@jridgewell/remapping@2.3.5": + dependencies: + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 + "@jridgewell/resolve-uri@3.1.2": {} "@jridgewell/source-map@0.3.11": @@ -5538,6 +5770,8 @@ snapshots: "@pkgjs/parseargs@0.11.0": optional: true + "@polka/url@1.0.0-next.29": {} + "@poppinss/colors@4.1.5": dependencies: kleur: 4.1.5 @@ -5706,22 +5940,19 @@ snapshots: "@types/node": 22.18.13 optional: true - "@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))": + "@vitest/coverage-istanbul@3.2.4(vitest@3.2.4)": dependencies: - "@ampproject/remapping": 2.3.0 - "@bcoe/v8-coverage": 1.0.2 - ast-v8-to-istanbul: 0.3.8 + "@istanbuljs/schema": 0.1.3 debug: 4.4.3 istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 - magic-string: 0.30.21 magicast: 0.3.5 - std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.18.13)(@vitest/ui@3.2.4)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -5761,6 +5992,17 @@ snapshots: dependencies: tinyspy: 4.0.4 + "@vitest/ui@3.2.4(vitest@3.2.4)": + dependencies: + "@vitest/utils": 3.2.4 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@22.18.13)(@vitest/ui@3.2.4)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + "@vitest/utils@3.2.4": dependencies: "@vitest/pretty-format": 3.2.4 @@ -5835,12 +6077,6 @@ snapshots: assertion-error@2.0.1: {} - ast-v8-to-istanbul@0.3.8: - dependencies: - "@jridgewell/trace-mapping": 0.3.31 - estree-walker: 3.0.3 - js-tokens: 9.0.1 - asynckit@0.4.0: {} available-typed-arrays@1.0.7: @@ -5856,6 +6092,8 @@ snapshots: balanced-match@1.0.2: {} + baseline-browser-mapping@2.9.4: {} + birpc@0.2.14: {} blake3-wasm@2.1.5: {} @@ -5873,6 +6111,14 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.4 + caniuse-lite: 1.0.30001759 + electron-to-chromium: 1.5.266 + node-releases: 2.0.27 + update-browserslist-db: 1.2.2(browserslist@4.28.1) + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -5902,6 +6148,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + caniuse-lite@1.0.30001759: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -5989,6 +6237,8 @@ snapshots: commander@2.20.3: optional: true + convert-source-map@2.0.0: {} + cookie@1.0.2: {} cross-inspect@1.0.1: @@ -6069,9 +6319,8 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.44.7(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.4)(bun-types@1.3.1(@types/react@19.2.2)): + drizzle-orm@0.44.7(@libsql/client@0.15.4)(bun-types@1.3.1(@types/react@19.2.2)): optionalDependencies: - "@cloudflare/workers-types": 4.20251014.0 "@libsql/client": 0.15.4 bun-types: 1.3.1(@types/react@19.2.2) @@ -6087,6 +6336,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + electron-to-chromium@1.5.266: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -6241,6 +6492,8 @@ snapshots: "@esbuild/win32-ia32": 0.27.0 "@esbuild/win32-x64": 0.27.0 + escalade@3.2.0: {} + estree-walker@3.0.3: dependencies: "@types/estree": 1.0.8 @@ -6289,10 +6542,14 @@ snapshots: web-streams-polyfill: 3.3.3 optional: true + fflate@0.8.2: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + flatted@3.3.3: {} + follow-redirects@1.15.11: {} for-each@0.3.5: @@ -6342,6 +6599,8 @@ snapshots: generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} + get-east-asian-width@1.4.0: {} get-intrinsic@1.3.0: @@ -6532,6 +6791,16 @@ snapshots: istanbul-lib-coverage@3.2.2: {} + istanbul-lib-instrument@6.0.3: + dependencies: + "@babel/core": 7.28.5 + "@babel/parser": 7.28.5 + "@istanbuljs/schema": 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 @@ -6574,6 +6843,8 @@ snapshots: jsesc@3.1.0: {} + json5@2.2.3: {} + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -6647,6 +6918,10 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + luxon@3.7.2: {} magic-string@0.30.21: @@ -6730,6 +7005,8 @@ snapshots: mkdirp@3.0.1: {} + mrmime@2.0.1: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -6747,6 +7024,8 @@ snapshots: formdata-polyfill: 4.0.10 optional: true + node-releases@2.0.27: {} + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -6882,6 +7161,8 @@ snapshots: safer-buffer@2.1.2: {} + semver@6.3.1: {} + semver@7.7.3: {} set-function-length@1.2.2: @@ -6933,6 +7214,12 @@ snapshots: dependencies: is-arrayish: 0.3.4 + sirv@3.0.2: + dependencies: + "@polka/url": 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.3 @@ -7035,6 +7322,8 @@ snapshots: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + tr46@0.0.3: {} ts-morph@22.0.0: @@ -7069,6 +7358,12 @@ snapshots: dependencies: pathe: 2.0.3 + update-browserslist-db@1.2.2(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + urlpattern-polyfill@10.1.0: {} util@0.12.5: @@ -7128,7 +7423,7 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 - vitest@3.2.4(@types/node@22.18.13)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): + vitest@3.2.4(@types/node@22.18.13)(@vitest/ui@3.2.4)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: "@types/chai": 5.2.3 "@vitest/expect": 3.2.4 @@ -7155,6 +7450,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: "@types/node": 22.18.13 + "@vitest/ui": 3.2.4(vitest@3.2.4) transitivePeerDependencies: - jiti - less @@ -7222,7 +7518,7 @@ snapshots: "@cloudflare/workerd-linux-arm64": 1.20251125.0 "@cloudflare/workerd-windows-64": 1.20251125.0 - wrangler@4.48.0(@cloudflare/workers-types@4.20251014.0): + wrangler@4.48.0: dependencies: "@cloudflare/kv-asset-handler": 0.4.0 "@cloudflare/unenv-preset": 2.7.10(unenv@2.0.0-rc.24)(workerd@1.20251109.0) @@ -7233,13 +7529,12 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20251109.0 optionalDependencies: - "@cloudflare/workers-types": 4.20251014.0 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil - utf-8-validate - wrangler@4.51.0(@cloudflare/workers-types@4.20251014.0): + wrangler@4.51.0: dependencies: "@cloudflare/kv-asset-handler": 0.4.1 "@cloudflare/unenv-preset": 2.7.11(unenv@2.0.0-rc.24)(workerd@1.20251125.0) @@ -7250,7 +7545,6 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20251125.0 optionalDependencies: - "@cloudflare/workers-types": 4.20251014.0 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil @@ -7279,6 +7573,8 @@ snapshots: ws@8.18.3: optional: true + yallist@3.1.1: {} + yaml@2.8.1: {} youch-core@0.3.3: diff --git a/src/consumet.ts b/src/consumet.ts deleted file mode 100644 index cdb0e7d..0000000 --- a/src/consumet.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ANIME, META } from "@consumet/extensions"; -import fetchAdapter from "@haverstack/axios-fetch-adapter"; - -const gogoAnime = new ANIME.Gogoanime(undefined, undefined, fetchAdapter); -export const aniList = new META.Anilist(gogoAnime, undefined, fetchAdapter); diff --git a/src/libs/getCurrentDomain.ts b/src/libs/getCurrentDomain.ts deleted file mode 100644 index 8c3b4c2..0000000 --- a/src/libs/getCurrentDomain.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { HonoRequest } from "hono"; - -export function getCurrentDomain(req: HonoRequest): string | undefined; -export function getCurrentDomain( - req: HonoRequest, - avoidLocalhost: false, -): string; -export function getCurrentDomain( - req: HonoRequest, - avoidLocalhost: true, -): string | undefined; -export function getCurrentDomain(req: HonoRequest, avoidLocalhost = true) { - let domain = req.url.replace(req.path, ""); - if (domain.includes("?")) { - domain = domain.split("?")[0]; - } - - if (avoidLocalhost) { - if ( - domain.includes("localhost") || - domain.includes("127.0.0.1") || - domain.includes("192.168.1") - ) { - console.log("Domain is localhost, returning undefined"); - return; - } - } - - return domain; -} diff --git a/src/libs/logStep.ts b/src/libs/logStep.ts deleted file mode 100644 index 97f9504..0000000 --- a/src/libs/logStep.ts +++ /dev/null @@ -1,23 +0,0 @@ -export async function logStep( - inProgressText: string, - step: () => Promise | T, -): Promise; -export async function logStep( - inProgressText: string, - step: () => Promise | T, - doneText: string, -): Promise; - -export async function logStep( - inProgressText: string, - step: () => Promise | T, - doneText: string = `Completed step "${inProgressText}"`, -) { - console.time(doneText); - console.log(`${inProgressText}...`); - - return Promise.resolve(step()).then((value) => { - console.timeEnd(doneText); - return value; - }); -} diff --git a/src/libs/readEnvVariable.spec.ts b/src/libs/readEnvVariable.spec.ts index f889987..96cfd02 100644 --- a/src/libs/readEnvVariable.spec.ts +++ b/src/libs/readEnvVariable.spec.ts @@ -30,6 +30,6 @@ describe("readEnvVariable", () => { }); it("env not defined, returns default value", () => { - expect(readEnvVariable("ENABLE_ANIFY", undefined)).toBe(true); + expect(readEnvVariable("ENABLE_ANIFY", {})).toBe(true); }); }); diff --git a/src/models/db.ts b/src/models/db.ts index 03f45eb..2f8b18c 100644 --- a/src/models/db.ts +++ b/src/models/db.ts @@ -1,15 +1,9 @@ -// import { createClient } from "@libsql/client"; import { env as cloudflareEnv } from "cloudflare:workers"; import { drizzle } from "drizzle-orm/d1"; type Db = ReturnType; -// let db: Db | null = null; export function getDb(env: Cloudflare.Env = cloudflareEnv): Db { - // if (db) { - // return db; - // } - const db = drizzle(env.DB, { logger: true }); return db; } diff --git a/src/resolvers/mutations/markEpisodeAsWatched.spec.ts b/src/resolvers/mutations/markEpisodeAsWatched.spec.ts new file mode 100644 index 0000000..3511f60 --- /dev/null +++ b/src/resolvers/mutations/markEpisodeAsWatched.spec.ts @@ -0,0 +1,109 @@ +import { GraphQLError } from "graphql"; +import { describe, expect, it, vi } from "vitest"; + +import { markEpisodeAsWatched } from "~/services/episodes/markEpisodeAsWatched/anilist"; + +import { markEpisodeAsWatchedMutation } from "./markEpisodeAsWatched"; + +vi.mock("~/services/episodes/markEpisodeAsWatched/anilist", () => ({ + markEpisodeAsWatched: vi.fn(), +})); + +vi.mock("~/services/watch-status", () => ({ + updateWatchStatus: vi.fn(), +})); + +describe("markEpisodeAsWatched mutation", () => { + it("should throw GraphQLError if aniListToken is missing", async () => { + await expect( + markEpisodeAsWatchedMutation( + null, + { input: { titleId: 1, episodeNumber: 1, isComplete: false } }, + { aniListToken: undefined } as any, + ), + ).rejects.toThrow( + new GraphQLError( + "AniList token is required. Please provide X-AniList-Token header.", + { + extensions: { code: "UNAUTHORIZED" }, + }, + ), + ); + }); + + it("should call markEpisodeAsWatched service", async () => { + vi.mocked(markEpisodeAsWatched).mockResolvedValue({} as any); + + await markEpisodeAsWatchedMutation( + null, + { input: { titleId: 1, episodeNumber: 1, isComplete: false } }, + { aniListToken: "token" } as any, + ); + + expect(markEpisodeAsWatched).toHaveBeenCalledWith("token", 1, 1, false); + }); + + it("should update watch status locally if isComplete is true and deviceId is present", async () => { + vi.mocked(markEpisodeAsWatched).mockResolvedValue({} as any); + const { updateWatchStatus } = await import("~/services/watch-status"); + + await markEpisodeAsWatchedMutation( + null, + { input: { titleId: 1, episodeNumber: 1, isComplete: true } }, + { aniListToken: "token", deviceId: "device-id" } as any, + ); + + expect(updateWatchStatus).toHaveBeenCalledWith("device-id", 1, "COMPLETED"); + }); + + it("should not update watch status locally if deviceId is missing", async () => { + vi.mocked(markEpisodeAsWatched).mockResolvedValue({} as any); + const { updateWatchStatus } = await import("~/services/watch-status"); + vi.mocked(updateWatchStatus).mockClear(); + + const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await markEpisodeAsWatchedMutation( + null, + { input: { titleId: 1, episodeNumber: 1, isComplete: true } }, + { aniListToken: "token" } as any, + ); + + expect(updateWatchStatus).not.toHaveBeenCalled(); + expect(consoleSpy).toHaveBeenCalledWith( + "Device ID not found in context, skipping watch status update", + ); + }); + + it("should throw GraphQLError if service return null", async () => { + vi.mocked(markEpisodeAsWatched).mockResolvedValue(null as any); + + await expect( + markEpisodeAsWatchedMutation( + null, + { input: { titleId: 1, episodeNumber: 1, isComplete: false } }, + { aniListToken: "token" } as any, + ), + ).rejects.toThrow( + new GraphQLError("Failed to mark episode as watched", { + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }), + ); + }); + + it("should catch errors and throw GraphQLError", async () => { + vi.mocked(markEpisodeAsWatched).mockRejectedValue(new Error("Foo")); + + await expect( + markEpisodeAsWatchedMutation( + null, + { input: { titleId: 1, episodeNumber: 1, isComplete: false } }, + { aniListToken: "token" } as any, + ), + ).rejects.toThrow( + new GraphQLError("Failed to mark episode as watched", { + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }), + ); + }); +}); diff --git a/src/resolvers/mutations/updateWatchStatus.spec.ts b/src/resolvers/mutations/updateWatchStatus.spec.ts new file mode 100644 index 0000000..5f509d4 --- /dev/null +++ b/src/resolvers/mutations/updateWatchStatus.spec.ts @@ -0,0 +1,55 @@ +import { GraphQLError } from "graphql"; +import { describe, expect, it, vi } from "vitest"; + +import { updateWatchStatus } from "~/services/watch-status"; + +import { updateWatchStatusMutation } from "./updateWatchStatus"; + +vi.mock("~/services/watch-status", () => ({ + updateWatchStatus: vi.fn(), +})); + +describe("updateWatchStatus mutation", () => { + it("should throw GraphQLError if deviceId is missing", async () => { + await expect( + updateWatchStatusMutation( + null, + { input: { titleId: 1, watchStatus: "CURRENT" } }, + { deviceId: undefined } as any, + ), + ).rejects.toThrow( + new GraphQLError( + "Device ID is required. Please provide X-Device-ID header.", + { + extensions: { code: "BAD_REQUEST" }, + }, + ), + ); + }); + + it("should call updateWatchStatus service with correct parameters", async () => { + await updateWatchStatusMutation( + null, + { input: { titleId: 1, watchStatus: "CURRENT" } }, + { deviceId: "device-id" } as any, + ); + + expect(updateWatchStatus).toHaveBeenCalledWith("device-id", 1, "CURRENT"); + }); + + it("should catch service errors and throw GraphQLError", async () => { + vi.mocked(updateWatchStatus).mockRejectedValue(new Error("Service error")); + + await expect( + updateWatchStatusMutation( + null, + { input: { titleId: 1, watchStatus: "CURRENT" } }, + { deviceId: "device-id" } as any, + ), + ).rejects.toThrow( + new GraphQLError("Failed to update watch status", { + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }), + ); + }); +}); diff --git a/src/resolvers/queries/home.spec.ts b/src/resolvers/queries/home.spec.ts new file mode 100644 index 0000000..dcdb2e5 --- /dev/null +++ b/src/resolvers/queries/home.spec.ts @@ -0,0 +1,91 @@ +import { env } from "cloudflare:workers"; +import { GraphQLError } from "graphql"; +import { describe, expect, it, vi } from "vitest"; + +import type { GraphQLContext } from "~/context"; + +import { home } from "./home"; + +enum HomeCategory { + WATCHING, + PLANNING, +} + +describe("home resolver", () => { + const mockContext = { + user: { name: "testuser" }, + aniListToken: "test-token", + } as GraphQLContext; + + it("should fetch WATCHING titles using CURRENT status filter", async () => { + const mockResponse = { some: "data" }; + const mockStub = { + getTitles: vi.fn().mockResolvedValue(mockResponse), + }; + + // @ts-expect-error - Partial mock + env.ANILIST_DO = { + getByName: vi.fn().mockResolvedValue(mockStub), + }; + + const result = await home( + null, + { category: HomeCategory.WATCHING }, + mockContext, + ); + + expect(result).toEqual(mockResponse); + expect(env.ANILIST_DO.getByName).toHaveBeenCalledWith("GLOBAL"); + expect(mockStub.getTitles).toHaveBeenCalledWith( + "testuser", + 1, + ["CURRENT"], + "test-token", + ); + }); + + it("should fetch PLANNING titles using PLANNING, PAUSED, REPEATING status filters", async () => { + const mockResponse = { some: "data" }; + const mockStub = { + getTitles: vi.fn().mockResolvedValue(mockResponse), + }; + + // @ts-expect-error - Partial mock + env.ANILIST_DO = { + getByName: vi.fn().mockResolvedValue(mockStub), + }; + + const result = await home( + null, + { category: HomeCategory.PLANNING, page: 2 }, + mockContext, + ); + + expect(result).toEqual(mockResponse); + expect(mockStub.getTitles).toHaveBeenCalledWith( + "testuser", + 2, + ["PLANNING", "PAUSED", "REPEATING"], + "test-token", + ); + }); + + it("should throw GraphQLError if Durable Object response is null", async () => { + const mockStub = { + getTitles: vi.fn().mockResolvedValue(null), + }; + + // @ts-expect-error - Partial mock + env.ANILIST_DO = { + getByName: vi.fn().mockResolvedValue(mockStub), + }; + + await expect( + home(null, { category: HomeCategory.WATCHING }, mockContext), + ).rejects.toThrow( + new GraphQLError("Failed to fetch 0 titles", { + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }), + ); + }); +}); diff --git a/src/resolvers/queries/popularBrowse.spec.ts b/src/resolvers/queries/popularBrowse.spec.ts new file mode 100644 index 0000000..413128f --- /dev/null +++ b/src/resolvers/queries/popularBrowse.spec.ts @@ -0,0 +1,51 @@ +import { GraphQLError } from "graphql"; +import { describe, expect, it, vi } from "vitest"; + +import { fetchPopularTitlesFromAnilist } from "~/services/popular/browse/anilist"; + +import { popularBrowse } from "./popularBrowse"; + +vi.mock("~/services/popular/browse/anilist", () => ({ + fetchPopularTitlesFromAnilist: vi.fn(), +})); + +describe("popularBrowse resolver", () => { + it("should fetch titles with default limit", async () => { + const mockResponse = { + trending: ["trending"], + popular: ["popular"], + upcoming: ["upcoming"], + }; + vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue(mockResponse); + + const result = await popularBrowse(null, {}, {} as any); + + expect(result).toEqual(mockResponse); + expect(fetchPopularTitlesFromAnilist).toHaveBeenCalledWith(10); + }); + + it("should fetch titles with provided limit", async () => { + const mockResponse = {}; + vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue(mockResponse); + + await popularBrowse(null, { limit: 20 }, {} as any); + + expect(fetchPopularTitlesFromAnilist).toHaveBeenCalledWith(20); + }); + + it("should throw GraphQLError if service returns null", async () => { + vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue(undefined); + + await expect(popularBrowse(null, {}, {} as any)).rejects.toThrow( + new GraphQLError("Failed to fetch popular titles", { + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }), + ); + }); + + it("should map response correctly to trending, popular, and upcoming", async () => { + vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue({} as any); + const result = await popularBrowse(null, {}, {} as any); + expect(result).toEqual({ trending: [], popular: [], upcoming: [] }); + }); +}); diff --git a/src/resolvers/queries/popularByCategory.spec.ts b/src/resolvers/queries/popularByCategory.spec.ts new file mode 100644 index 0000000..638d53e --- /dev/null +++ b/src/resolvers/queries/popularByCategory.spec.ts @@ -0,0 +1,59 @@ +import { GraphQLError } from "graphql"; +import { describe, expect, it, vi } from "vitest"; + +import { fetchPopularTitlesFromAnilist } from "~/services/popular/category/anilist"; + +import { popularByCategory } from "./popularByCategory"; + +vi.mock("~/services/popular/category/anilist", () => ({ + fetchPopularTitlesFromAnilist: vi.fn(), +})); + +describe("popularByCategory resolver", () => { + it("should fetch titles for a specific category with page and limit", async () => { + const mockResponse = { results: ["title"], hasNextPage: true }; + vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue( + mockResponse as any, + ); + + const result = await popularByCategory( + null, + { category: "trending", page: 2, limit: 20 }, + {} as any, + ); + + expect(result).toEqual(mockResponse); + expect(fetchPopularTitlesFromAnilist).toHaveBeenCalledWith( + "trending", + 2, + 20, + ); + }); + + it("should use default page and limit", async () => { + const mockResponse = { results: [], hasNextPage: false }; + vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue( + mockResponse as any, + ); + + await popularByCategory(null, { category: "popular" }, {} as any); + + expect(fetchPopularTitlesFromAnilist).toHaveBeenCalledWith( + "popular", + 1, + 10, + ); + }); + + it("should throw GraphQLError if service returns null", async () => { + vi.mocked(fetchPopularTitlesFromAnilist).mockResolvedValue(undefined); + + await expect( + popularByCategory(null, { category: "upcoming" }, {} as any), + ).rejects.toThrow( + new GraphQLError("Failed to fetch upcoming titles", { + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }), + ); + }); +}); diff --git a/src/resolvers/queries/user.spec.ts b/src/resolvers/queries/user.spec.ts new file mode 100644 index 0000000..6f8eec8 --- /dev/null +++ b/src/resolvers/queries/user.spec.ts @@ -0,0 +1,44 @@ +import { GraphQLError } from "graphql"; +import { describe, expect, it, vi } from "vitest"; + +import { getUser } from "~/services/auth/anilist/getUser"; + +import { user } from "./user"; + +vi.mock("~/services/auth/anilist/getUser", () => ({ + getUser: vi.fn(), +})); + +describe("user resolver", () => { + it("should throw GraphQLError (UNAUTHORIZED) if aniListToken is missing", async () => { + await expect( + user(null, {}, { aniListToken: undefined } as any), + ).rejects.toThrow( + new GraphQLError("Unauthorized", { + extensions: { code: "UNAUTHORIZED" }, + }), + ); + }); + + it("should fetch user if token is present", async () => { + const mockUser = { id: 1, name: "test" }; + vi.mocked(getUser).mockResolvedValue(mockUser as any); + + const result = await user(null, {}, { aniListToken: "token" } as any); + + expect(result).toEqual(mockUser); + expect(getUser).toHaveBeenCalledWith("token"); + }); + + it("should throw GraphQLError if user service returns null", async () => { + vi.mocked(getUser).mockResolvedValue(null); + + await expect( + user(null, {}, { aniListToken: "token" } as any), + ).rejects.toThrow( + new GraphQLError("Failed to fetch user", { + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }), + ); + }); +}); diff --git a/src/scripts/generateEnv.ts b/src/scripts/generateEnv.ts deleted file mode 100644 index 0eba310..0000000 --- a/src/scripts/generateEnv.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { $ } from "bun"; -import { Project } from "ts-morph"; - -import { logStep } from "~/libs/logStep"; - -await logStep( - 'Re-generating "env.d.ts"', - () => $`bunx wrangler types src/types/env.d.ts`.quiet(), - "Generated env.d.ts", -); - -const secretNames = await logStep( - "Fetching secrets from Cloudflare", - async (): Promise => { - const { stdout } = await $`bunx wrangler secret list`.quiet(); - return JSON.parse(stdout.toString()).map( - (secret: { name: string; type: "secret_text" }) => secret.name, - ); - }, - "Fetched secrets", -); - -const project = new Project({}); - -const envSourceFile = project.addSourceFileAtPath("src/types/env.d.ts"); -envSourceFile.insertImportDeclaration(2, { - isTypeOnly: true, - moduleSpecifier: "hono", - namedImports: ["Env as HonoEnv"], -}); -envSourceFile - .getInterfaceOrThrow("Env") - .addExtends(["HonoEnv", "Record"]); -envSourceFile.getInterfaceOrThrow("Env").addProperties( - secretNames.map((name) => ({ - name, - type: `string`, - })), -); - -await project.save(); - -await logStep( - "Formatting env.d.ts", - () => $`bunx prettier --write src/types/env.d.ts`.quiet(), - "Formatted env.d.ts", -); diff --git a/src/scripts/verifyEnv.ts b/src/scripts/verifyEnv.ts deleted file mode 100644 index e8a68bc..0000000 --- a/src/scripts/verifyEnv.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { $, sleep, spawn } from "bun"; -import { readFile } from "fs/promises"; - -import { logStep } from "~/libs/logStep"; - -await $`cp src/types/env.d.ts /tmp/env.d.ts`.quiet(); - -await logStep( - 'Generating "env.d.ts"', - () => import("./generateEnv"), - "Generated env.d.ts", -); - -await logStep("Comparing env.d.ts", async () => { - function filterComments(content: Buffer) { - return content - .toString() - .split("\n") - .filter((line) => !line.trim().startsWith("//")) - .join("\n"); - } - - const currentFileContent = filterComments(await readFile("/tmp/env.d.ts")); - const generatedFileContent = filterComments( - await readFile("src/types/env.d.ts"), - ); - - if (currentFileContent === generatedFileContent) { - console.log("env.d.ts is up to date"); - return; - } - - const isCI = process.env["IS_CI"] === "true"; - const vcsCommand = isCI ? "git" : "sl"; - spawn({ - cmd: [vcsCommand, "diff", "src/types/env.d.ts"], - stdout: "inherit", - }); - // add 1 second to make sure spawn completes - await sleep(1000); - throw new Error("env.d.ts is out of date"); -}); diff --git a/src/services/auth/anilist/getUser.spec.ts b/src/services/auth/anilist/getUser.spec.ts new file mode 100644 index 0000000..e36497c --- /dev/null +++ b/src/services/auth/anilist/getUser.spec.ts @@ -0,0 +1,54 @@ +import { env } from "cloudflare:workers"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { getUser } from "./getUser"; + +describe("getUser service", () => { + const mockStub = { + getUserProfile: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + // @ts-expect-error - Partial mock + env.ANILIST_DO = { + idFromName: vi.fn().mockReturnValue("global-id"), + get: vi.fn().mockReturnValue(mockStub), + }; + }); + + it("should fetch user profile from Durable Object", async () => { + const mockUser = { id: 1, name: "User", statistics: { anime: {} } }; + mockStub.getUserProfile.mockResolvedValue(mockUser); + + const result = await getUser("token"); + + expect(result).toEqual({ + ...mockUser, + statistics: mockUser.statistics.anime, + }); + expect(mockStub.getUserProfile).toHaveBeenCalledWith("token"); + }); + + it("should return null if DO throws 401 error", async () => { + mockStub.getUserProfile.mockRejectedValue(new Error("401 Unauthorized")); + + const result = await getUser("token"); + + expect(result).toBeNull(); + }); + + it("should rethrow other DO errors", async () => { + mockStub.getUserProfile.mockRejectedValue(new Error("Other Error")); + + await expect(getUser("token")).rejects.toThrow("Other Error"); + }); + + it("should return null if DO returns null", async () => { + mockStub.getUserProfile.mockResolvedValue(null); + + const result = await getUser("token"); + + expect(result).toBeNull(); + }); +}); diff --git a/src/services/episodes/markEpisodeAsWatched/anilist.spec.ts b/src/services/episodes/markEpisodeAsWatched/anilist.spec.ts new file mode 100644 index 0000000..a788528 --- /dev/null +++ b/src/services/episodes/markEpisodeAsWatched/anilist.spec.ts @@ -0,0 +1,64 @@ +import { env } from "cloudflare:workers"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { markEpisodeAsWatched } from "./anilist"; + +describe("markEpisodeAsWatched service", () => { + const mockStub = { + markTitleAsWatched: vi.fn(), + markEpisodeAsWatched: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + // @ts-expect-error - Partial mock + env.ANILIST_DO = { + idFromName: vi.fn().mockReturnValue("global-id"), + get: vi.fn().mockReturnValue(mockStub), + }; + }); + + it("should call markTitleAsWatched on DO if markTitleAsComplete is true", async () => { + mockStub.markTitleAsWatched.mockResolvedValue({ + user: { id: 1, statistics: { anime: {} } }, + }); + + const result = await markEpisodeAsWatched("token", 1, 12, true); + + expect(result).toBeDefined(); + expect(mockStub.markTitleAsWatched).toHaveBeenCalledWith(1, "token"); + expect(mockStub.markEpisodeAsWatched).not.toHaveBeenCalled(); + }); + + it("should call markEpisodeAsWatched on DO if markTitleAsComplete is false", async () => { + mockStub.markEpisodeAsWatched.mockResolvedValue({ + user: { id: 1, statistics: { anime: {} } }, + }); + + const result = await markEpisodeAsWatched("token", 1, 12, false); + + expect(result).toBeDefined(); + expect(mockStub.markEpisodeAsWatched).toHaveBeenCalledWith(1, 12, "token"); + expect(mockStub.markTitleAsWatched).not.toHaveBeenCalled(); + }); + + it("should throw error if DO returns null", async () => { + mockStub.markEpisodeAsWatched.mockResolvedValue(null); + + await expect(markEpisodeAsWatched("token", 1, 12, false)).rejects.toThrow( + "Failed to mark episode as watched", + ); + }); + + it("should return formatted user data", async () => { + const mockUser = { id: 1, statistics: { anime: { count: 10 } } }; + mockStub.markEpisodeAsWatched.mockResolvedValue({ user: mockUser }); + + const result = await markEpisodeAsWatched("token", 1, 12, false); + + expect(result).toEqual({ + ...mockUser, + statistics: { count: 10 }, + }); + }); +}); diff --git a/src/services/popular/browse/anilist.spec.ts b/src/services/popular/browse/anilist.spec.ts new file mode 100644 index 0000000..90980e9 --- /dev/null +++ b/src/services/popular/browse/anilist.spec.ts @@ -0,0 +1,84 @@ +import { env } from "cloudflare:workers"; +import { describe, expect, it, vi } from "vitest"; + +import { fetchPopularTitlesFromAnilist } from "./anilist"; + +// Mock getCurrentAndNextSeason +vi.mock("~/libs/getCurrentAndNextSeason", () => ({ + getCurrentAndNextSeason: vi.fn(() => ({ + current: { season: "WINTER", year: 2024 }, + next: { season: "SPRING", year: 2024 }, + })), +})); + +vi.mock("../mapTitle", () => ({ + mapTitle: vi.fn((title) => ({ id: title.id, title: title.title })), +})); + +describe("fetchPopularTitlesFromAnilist (Browse)", () => { + it("should fetch popular titles from Durable Object with current/next season info", async () => { + const mockReponse = { + trending: { media: [{ id: 1, title: "Trending" }] }, + season: { media: [{ id: 2, title: "Popular" }] }, + nextSeason: { + media: [ + { + id: 3, + title: "Upcoming", + nextAiringEpisode: { airingAt: 123 }, + }, + ], + }, + }; + const mockNextSeasonResponse = { + Page: { media: [{ id: 4, title: "Next Season Popular" }] }, + }; + + const mockStub = { + browsePopular: vi.fn().mockResolvedValue(mockReponse), + nextSeasonPopular: vi.fn().mockResolvedValue(mockNextSeasonResponse), + }; + + // @ts-expect-error - Partial mock + env.ANILIST_DO = { + getByName: vi.fn().mockReturnValue(mockStub), + }; + + const result = await fetchPopularTitlesFromAnilist(10); + + expect(result.trending).toHaveLength(1); + expect(result.popular).toHaveLength(1); + expect(result.upcoming).toHaveLength(1); + expect(mockStub.browsePopular).toHaveBeenCalledWith( + "WINTER", + 2024, + "SPRING", + 2024, + 10, + ); + expect(mockStub.nextSeasonPopular).toHaveBeenCalledWith("SPRING", 2024, 10); + }); + + it("should handle missing next season data gracefully (return only trending/popular)", async () => { + const mockReponse = { + trending: { media: [{ id: 1, title: "Trending" }] }, + season: { media: [{ id: 2, title: "Popular" }] }, + nextSeason: { media: [] }, + }; + + const mockStub = { + browsePopular: vi.fn().mockResolvedValue(mockReponse), + }; + + // @ts-expect-error - Partial mock + env.ANILIST_DO = { + getByName: vi.fn().mockReturnValue(mockStub), + }; + + const result = await fetchPopularTitlesFromAnilist(10); + + expect(result.trending).toHaveLength(1); + expect(result.popular).toHaveLength(1); + expect(result.upcoming).toBeUndefined(); // Or check implementation if it returns empty array or undefined + }); +}); diff --git a/src/services/popular/category/anilist.spec.ts b/src/services/popular/category/anilist.spec.ts new file mode 100644 index 0000000..9910291 --- /dev/null +++ b/src/services/popular/category/anilist.spec.ts @@ -0,0 +1,90 @@ +import { env } from "cloudflare:workers"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { fetchPopularTitlesFromAnilist } from "./anilist"; + +// Mock getCurrentAndNextSeason +vi.mock("~/libs/getCurrentAndNextSeason", () => ({ + getCurrentAndNextSeason: vi.fn(() => ({ + current: { season: "WINTER", year: 2024 }, + next: { season: "SPRING", year: 2024 }, + })), +})); + +vi.mock("../mapTitle", () => ({ + mapTitle: vi.fn((title) => ({ id: title.id, title: title.title })), +})); + +describe("fetchPopularTitlesFromAnilist (Category)", () => { + const mockStub = { + getTrendingTitles: vi.fn(), + getPopularTitles: vi.fn(), + nextSeasonPopular: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + // @ts-expect-error - Partial mock + env.ANILIST_DO = { + idFromName: vi.fn().mockReturnValue("global-id"), + get: vi.fn().mockReturnValue(mockStub), + }; + }); + + it("should fetch 'trending' titles from Durable Object", async () => { + mockStub.getTrendingTitles.mockResolvedValue({ + media: [{ id: 1, title: "Trending" }], + pageInfo: { hasNextPage: true }, + }); + + const result = await fetchPopularTitlesFromAnilist("trending", 1, 10); + + expect(result.results).toHaveLength(1); + expect(result.hasNextPage).toBe(true); + expect(mockStub.getTrendingTitles).toHaveBeenCalledWith(1, 10); + }); + + it("should fetch 'popular' titles from Durable Object", async () => { + mockStub.getPopularTitles.mockResolvedValue({ + media: [{ id: 2, title: "Popular" }], + pageInfo: { hasNextPage: false }, + }); + + const result = await fetchPopularTitlesFromAnilist("popular", 1, 10); + + expect(result.results).toHaveLength(1); + expect(mockStub.getPopularTitles).toHaveBeenCalledWith( + 1, + 10, + "WINTER", + 2024, + ); + }); + + it("should fetch 'upcoming' titles from Durable Object", async () => { + mockStub.nextSeasonPopular.mockResolvedValue({ + media: [{ id: 3, title: "Upcoming" }], + pageInfo: { hasNextPage: true }, + }); + + const result = await fetchPopularTitlesFromAnilist("upcoming", 1, 10); + + expect(result.results).toHaveLength(1); + expect(mockStub.nextSeasonPopular).toHaveBeenCalledWith("SPRING", 2024, 10); + }); + + it("should throw error for unknown category", async () => { + await expect( + fetchPopularTitlesFromAnilist("unknown" as any, 1, 10), + ).rejects.toThrow("Unknown category: unknown"); + }); + + it("should return empty results if DO returns null", async () => { + mockStub.getTrendingTitles.mockResolvedValue(null); + + const result = await fetchPopularTitlesFromAnilist("trending", 1, 10); + + expect(result.results).toEqual([]); + expect(result.hasNextPage).toBe(false); + }); +}); diff --git a/src/services/watch-status/anilist.spec.ts b/src/services/watch-status/anilist.spec.ts new file mode 100644 index 0000000..b5d62cf --- /dev/null +++ b/src/services/watch-status/anilist.spec.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi } from "vitest"; + +import { maybeUpdateWatchStatusOnAnilist } from "./anilist"; + +const mockRequest = vi.fn(); +vi.mock("graphql-request", () => ({ + GraphQLClient: vi.fn(() => ({ + request: mockRequest, + })), +})); + +vi.mock("~/libs/errors/TitleNotFound", () => ({ + AnilistTitleNotFoundError: class extends Error { + constructor() { + super("AnilistTitleNotFoundError"); + } + }, +})); + +describe("maybeUpdateWatchStatusOnAnilist service", () => { + it("should return true immediately if token is missing", async () => { + const result = await maybeUpdateWatchStatusOnAnilist( + 1, + "CURRENT", + undefined, + ); + expect(result).toBe(true); + }); + + it("should perform SaveMediaListEntry mutation if watch status is provided", async () => { + mockRequest.mockResolvedValue({ SaveMediaListEntry: { id: 123 } }); + + const result = await maybeUpdateWatchStatusOnAnilist(1, "CURRENT", "token"); + + expect(result).toBe(true); + expect(mockRequest).toHaveBeenCalledWith( + expect.anything(), + { titleId: 1, watchStatus: "CURRENT" }, + expect.anything(), + ); + }); + + it("should perform DeleteMediaListEntry if watch status is null", async () => { + mockRequest + .mockResolvedValueOnce({ Media: { mediaListEntry: { id: 456 } } }) // Fetch ID + .mockResolvedValueOnce({ DeleteMediaListEntry: { deleted: true } }); // Delete + + const result = await maybeUpdateWatchStatusOnAnilist(1, null, "token"); + + expect(result).toBe(true); + // First call to get ID + expect(mockRequest).toHaveBeenCalledWith( + expect.anything(), + { titleId: 1 }, + expect.anything(), + ); + // Second call to delete + expect(mockRequest).toHaveBeenCalledWith( + expect.anything(), + { entryId: 456 }, + expect.anything(), + ); + }); + + it("should throw AnilistTitleNotFoundError if trying to delete non-existent entry", async () => { + mockRequest.mockResolvedValueOnce({ Media: { mediaListEntry: null } }); + + await expect( + maybeUpdateWatchStatusOnAnilist(1, null, "token"), + ).rejects.toThrow("AnilistTitleNotFoundError"); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 334dbd8..9331c4c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "types": ["@cloudflare/workers-types"], "baseUrl": "./", "paths": { "~/*": ["src/*"] diff --git a/vitest.config.ts b/vitest.config.ts index 04e8dd2..a7b9f2c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,31 +1,47 @@ -import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; +import { + defineWorkersProject, + readD1Migrations, +} from "@cloudflare/vitest-pool-workers/config"; import tsconfigPaths from "vite-tsconfig-paths"; import { configDefaults } from "vitest/config"; -export default defineWorkersConfig({ - plugins: [tsconfigPaths()], - test: { - globals: false, - setupFiles: ["./testSetup.ts"], - poolOptions: { - workers: { - wrangler: { configPath: "./wrangler.toml" }, +import path from "node:path"; + +export default defineWorkersProject(async () => { + const migrationsPath = path.join(__dirname, "drizzle"); + let migrations: Awaited>; + try { + migrations = await readD1Migrations(migrationsPath); + } catch (e) { + console.warn("Could not read migrations", e); + migrations = []; + } + + return { + plugins: [tsconfigPaths()], + test: { + globals: false, + setupFiles: ["./testSetup.ts"], + poolOptions: { + workers: { + wrangler: { configPath: "./wrangler.toml" }, + }, + }, + coverage: { + ...configDefaults.coverage, + provider: "istanbul", + reporter: ["text", "json", "html"], + exclude: [ + ...configDefaults.coverage.exclude, + "node_modules/**", + "dist/**", + "**/*.spec.ts", + "**/*.d.ts", + "**/mocks/**", + "drizzle.config.ts", + "src/schema.ts", + ], }, }, - coverage: { - ...configDefaults.coverage, - provider: "v8", - reporter: ["text", "json", "html"], - exclude: [ - ...configDefaults.coverage.exclude, - "node_modules/**", - "dist/**", - "**/*.spec.ts", - "**/*.d.ts", - "**/mocks/**", - "drizzle.config.ts", - "src/schema.ts", - ], - }, - }, + }; });