feat: Add unit tests for resolvers and services, update dependencies, and remove unused scripts.

This commit is contained in:
2025-12-07 05:18:23 -05:00
parent 83732913f7
commit 4b3354a5d6
22 changed files with 1204 additions and 275 deletions

View File

@@ -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",

474
pnpm-lock.yaml generated
View File

@@ -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:

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -1,23 +0,0 @@
export async function logStep<T = void>(
inProgressText: string,
step: () => Promise<T> | T,
): Promise<T>;
export async function logStep<T = void>(
inProgressText: string,
step: () => Promise<T> | T,
doneText: string,
): Promise<T>;
export async function logStep<T = void>(
inProgressText: string,
step: () => Promise<T> | T,
doneText: string = `Completed step "${inProgressText}"`,
) {
console.time(doneText);
console.log(`${inProgressText}...`);
return Promise.resolve(step()).then((value) => {
console.timeEnd(doneText);
return value;
});
}

View File

@@ -30,6 +30,6 @@ describe("readEnvVariable", () => {
});
it("env not defined, returns default value", () => {
expect(readEnvVariable<boolean>("ENABLE_ANIFY", undefined)).toBe(true);
expect(readEnvVariable<boolean>("ENABLE_ANIFY", {})).toBe(true);
});
});

View File

@@ -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<typeof drizzle>;
// 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;
}

View File

@@ -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" },
}),
);
});
});

View File

@@ -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" },
}),
);
});
});

View File

@@ -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" },
}),
);
});
});

View File

@@ -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: [] });
});
});

View File

@@ -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" },
}),
);
});
});

View File

@@ -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" },
}),
);
});
});

View File

@@ -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<string[]> => {
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<string, unknown>"]);
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",
);

View File

@@ -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");
});

View File

@@ -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();
});
});

View File

@@ -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 },
});
});
});

View File

@@ -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
});
});

View File

@@ -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);
});
});

View File

@@ -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");
});
});

View File

@@ -1,6 +1,5 @@
{
"compilerOptions": {
"types": ["@cloudflare/workers-types"],
"baseUrl": "./",
"paths": {
"~/*": ["src/*"]

View File

@@ -1,8 +1,23 @@
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({
import path from "node:path";
export default defineWorkersProject(async () => {
const migrationsPath = path.join(__dirname, "drizzle");
let migrations: Awaited<ReturnType<typeof readD1Migrations>>;
try {
migrations = await readD1Migrations(migrationsPath);
} catch (e) {
console.warn("Could not read migrations", e);
migrations = [];
}
return {
plugins: [tsconfigPaths()],
test: {
globals: false,
@@ -14,7 +29,7 @@ export default defineWorkersConfig({
},
coverage: {
...configDefaults.coverage,
provider: "v8",
provider: "istanbul",
reporter: ["text", "json", "html"],
exclude: [
...configDefaults.coverage.exclude,
@@ -28,4 +43,5 @@ export default defineWorkersConfig({
],
},
},
};
});