diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4a950a..4a083cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,5 +20,8 @@ jobs: cache: npm - run: npm ci - run: npm run typecheck + - run: npm run lint - run: npm run test:unit - run: npm run build + - run: npx publint + - run: npx @arethetypeswrong/cli --pack . diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..4594b35 --- /dev/null +++ b/biome.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.5/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedImports": "warn", + "noUnusedVariables": "warn" + }, + "style": { + "noDefaultExport": "warn" + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 120 + }, + "files": { + "includes": ["src/**/*.ts", "test/**/*.ts"] + } +} diff --git a/package-lock.json b/package-lock.json index 8fdbeb5..5649e7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { + "@biomejs/biome": "^2.0.6", "@types/node": "^25.3.3", "@types/ws": "^8.18.1", + "@vitest/coverage-v8": "^4.0.18", "tsup": "^8.0.0", "tsx": "^4.0.0", "typescript": "^5.5.0", @@ -28,6 +30,229 @@ } } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.5.tgz", + "integrity": "sha512-OWNCyMS0Q011R6YifXNOg6qsOg64IVc7XX6SqGsrGszPbkVCoaO7Sr/lISFnXZ9hjQhDewwZ40789QmrG0GYgQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.5", + "@biomejs/cli-darwin-x64": "2.4.5", + "@biomejs/cli-linux-arm64": "2.4.5", + "@biomejs/cli-linux-arm64-musl": "2.4.5", + "@biomejs/cli-linux-x64": "2.4.5", + "@biomejs/cli-linux-x64-musl": "2.4.5", + "@biomejs/cli-win32-arm64": "2.4.5", + "@biomejs/cli-win32-x64": "2.4.5" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.5.tgz", + "integrity": "sha512-lGS4Nd5O3KQJ6TeWv10mElnx1phERhBxqGP/IKq0SvZl78kcWDFMaTtVK+w3v3lusRFxJY78n07PbKplirsU5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.5.tgz", + "integrity": "sha512-6MoH4tyISIBNkZ2Q5T1R7dLd5BsITb2yhhhrU9jHZxnNSNMWl+s2Mxu7NBF8Y3a7JJcqq9nsk8i637z4gqkJxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.5.tgz", + "integrity": "sha512-U1GAG6FTjhAO04MyH4xn23wRNBkT6H7NentHh+8UxD6ShXKBm5SY4RedKJzkUThANxb9rUKIPc7B8ew9Xo/cWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.5.tgz", + "integrity": "sha512-iqLDgpzobG7gpBF0fwEVS/LT8kmN7+S0E2YKFDtqliJfzNLnAiV2Nnyb+ehCDCJgAZBASkYHR2o60VQWikpqIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.5.tgz", + "integrity": "sha512-NdODlSugMzTlENPTa4z0xB82dTUlCpsrOxc43///aNkTLblIYH4XpYflBbf5ySlQuP8AA4AZd1qXhV07IdrHdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.5.tgz", + "integrity": "sha512-NlKa7GpbQmNhZf9kakQeddqZyT7itN7jjWdakELeXyTU3pg/83fTysRRDPJD0akTfKDl6vZYNT9Zqn4MYZVBOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.5.tgz", + "integrity": "sha512-EBfrTqRIWOFSd7CQb/0ttjHMR88zm3hGravnDwUA9wHAaCAYsULKDebWcN5RmrEo1KBtl/gDVJMrFjNR0pdGUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.5.tgz", + "integrity": "sha512-Pmhv9zT95YzECfjEHNl3mN9Vhusw9VA5KHY0ZvlGsxsjwS5cb7vpRnHzJIv0vG7jB0JI7xEaMH9ddfZm/RozBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -911,6 +1136,37 @@ "@types/node": "*" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -1052,6 +1308,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/bundle-require": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", @@ -1276,6 +1544,62 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -1286,6 +1610,13 @@ "node": ">=10" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -1326,6 +1657,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -1598,6 +1957,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1662,6 +2034,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", diff --git a/package.json b/package.json index 9830c3a..1d7e1da 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,14 @@ "types": "dist/index.d.ts", "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } }, "./package.json": "./package.json" }, @@ -21,8 +26,12 @@ "build": "tsup", "dev": "tsup --watch", "typecheck": "tsc --noEmit", + "lint": "biome check src/ test/", + "lint:fix": "biome check --write src/ test/", + "format": "biome format --write src/ test/", "test": "node --import tsx test/smoke.ts", "test:unit": "vitest run", + "test:coverage": "vitest run --coverage", "prepublishOnly": "npm run build" }, "keywords": [ @@ -59,8 +68,10 @@ "access": "public" }, "devDependencies": { + "@biomejs/biome": "^2.0.6", "@types/node": "^25.3.3", "@types/ws": "^8.18.1", + "@vitest/coverage-v8": "^4.0.18", "tsup": "^8.0.0", "tsx": "^4.0.0", "typescript": "^5.5.0", diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..5eeea41 --- /dev/null +++ b/renovate.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended", "group:allNonMajor"], + "schedule": ["before 9am on monday"], + "automerge": true, + "automergeType": "branch", + "packageRules": [ + { "matchUpdateTypes": ["major"], "automerge": false }, + { "matchDepTypes": ["devDependencies"], "automerge": true } + ] +} diff --git a/src/api/calendar.ts b/src/api/calendar.ts index 3236baf..1605595 100644 --- a/src/api/calendar.ts +++ b/src/api/calendar.ts @@ -1,11 +1,16 @@ -import type { HttpClient } from '../http.js'; +import type { HttpClient } from "../http.js"; import type { - CalendarEarningsParams, CalendarEarningsResponse, - CalendarTrendsParams, CalendarTrendsResponse, - CalendarIposParams, CalendarIposResponse, - CalendarSplitsParams, CalendarSplitsResponse, - CalendarDividendsParams, CalendarDividendsData, -} from '../types.js'; + CalendarDividendsData, + CalendarDividendsParams, + CalendarEarningsParams, + CalendarEarningsResponse, + CalendarIposParams, + CalendarIposResponse, + CalendarSplitsParams, + CalendarSplitsResponse, + CalendarTrendsParams, + CalendarTrendsResponse, +} from "../types.js"; /** * Calendar API for upcoming and historical corporate events. @@ -32,7 +37,7 @@ export class CalendarApi { * ``` */ async earnings(params: CalendarEarningsParams = {}): Promise { - return this.http.get('/calendar/earnings', params); + return this.http.get("/calendar/earnings", params); } /** @@ -50,7 +55,7 @@ export class CalendarApi { * ``` */ async trends(params: CalendarTrendsParams): Promise { - return this.http.get('/calendar/trends', params); + return this.http.get("/calendar/trends", params); } /** @@ -68,7 +73,7 @@ export class CalendarApi { * ``` */ async ipos(params: CalendarIposParams = {}): Promise { - return this.http.get('/calendar/ipos', params); + return this.http.get("/calendar/ipos", params); } /** @@ -86,7 +91,7 @@ export class CalendarApi { * ``` */ async splits(params: CalendarSplitsParams = {}): Promise { - return this.http.get('/calendar/splits', params); + return this.http.get("/calendar/splits", params); } /** @@ -104,6 +109,6 @@ export class CalendarApi { * ``` */ async dividends(params: CalendarDividendsParams = {}): Promise { - return this.http.get('/calendar/dividends', params); + return this.http.get("/calendar/dividends", params); } } diff --git a/src/api/cboe.ts b/src/api/cboe.ts index 3725607..4520f6d 100644 --- a/src/api/cboe.ts +++ b/src/api/cboe.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '../http.js'; -import type { CboeIndexParams, CboeIndexItem, CboeIndexData } from '../types.js'; +import type { HttpClient } from "../http.js"; +import type { CboeIndexData, CboeIndexItem, CboeIndexParams } from "../types.js"; /** * CBOE Europe indices API for index listings and historical data. @@ -25,7 +25,7 @@ export class CboeApi { * ``` */ async indices(): Promise { - return this.http.get('/cboe/indices'); + return this.http.get("/cboe/indices"); } /** @@ -47,6 +47,6 @@ export class CboeApi { * ``` */ async index(params: CboeIndexParams): Promise { - return this.http.get('/cboe/index', params); + return this.http.get("/cboe/index", params); } } diff --git a/src/api/corporate.ts b/src/api/corporate.ts index a93f7bf..a4a7df9 100644 --- a/src/api/corporate.ts +++ b/src/api/corporate.ts @@ -1,11 +1,11 @@ -import type { HttpClient } from '../http.js'; -import type { InsiderTransactionsParams, InsiderTransactionItem } from '../types.js'; +import type { HttpClient } from "../http.js"; +import type { InsiderTransactionItem, InsiderTransactionsParams } from "../types.js"; export class CorporateApi { constructor(private http: HttpClient) {} /** Insider transactions (SEC Form 4): GET /insider-transactions */ async insiderTransactions(params: InsiderTransactionsParams = {}): Promise { - return this.http.get('/insider-transactions', params); + return this.http.get("/insider-transactions", params); } } diff --git a/src/api/eod.ts b/src/api/eod.ts index 64e42e5..c1ecd7d 100644 --- a/src/api/eod.ts +++ b/src/api/eod.ts @@ -1,11 +1,23 @@ -import type { HttpClient } from '../http.js'; +import type { HttpClient } from "../http.js"; import type { - Ticker, DateRange, EodParams, EodDataPoint, IntradayParams, IntradayDataPoint, - RealTimeQuote, RealTimeParams, UsQuoteDelayedParams, UsQuoteDelayedResult, - BulkEodParams, BulkEodDataPoint, - DividendDataPoint, SplitDataPoint, HistoricalMarketCapPoint, - TicksParams, TickDataPoint, -} from '../types.js'; + BulkEodDataPoint, + BulkEodParams, + DateRange, + DividendDataPoint, + EodDataPoint, + EodParams, + HistoricalMarketCapPoint, + IntradayDataPoint, + IntradayParams, + RealTimeParams, + RealTimeQuote, + SplitDataPoint, + TickDataPoint, + Ticker, + TicksParams, + UsQuoteDelayedParams, + UsQuoteDelayedResult, +} from "../types.js"; export class EodApi { constructor(private http: HttpClient) {} @@ -27,7 +39,7 @@ export class EodApi { /** US extended delayed quotes: GET /us-quote-delayed */ async usQuoteDelayed(params: UsQuoteDelayedParams = {}): Promise { - return this.http.get('/us-quote-delayed', params); + return this.http.get("/us-quote-delayed", params); } /** Bulk EOD data for exchange: GET /eod-bulk-last-day/{exchange} */ @@ -52,6 +64,6 @@ export class EodApi { /** US stock market tick data: GET /ticks */ async ticks(params: TicksParams = {}): Promise { - return this.http.get('/ticks', params); + return this.http.get("/ticks", params); } } diff --git a/src/api/exchanges.ts b/src/api/exchanges.ts index a2feb04..0a90b95 100644 --- a/src/api/exchanges.ts +++ b/src/api/exchanges.ts @@ -1,5 +1,13 @@ -import type { HttpClient } from '../http.js'; -import type { Exchange, ExchangeSymbol, ExchangeDetails, ExchangeSymbolsParams, ExchangeDetailsParams, SymbolChangeItem, DateRange } from '../types.js'; +import type { HttpClient } from "../http.js"; +import type { + DateRange, + Exchange, + ExchangeDetails, + ExchangeDetailsParams, + ExchangeSymbol, + ExchangeSymbolsParams, + SymbolChangeItem, +} from "../types.js"; /** * Exchanges API for exchange listings, symbols, details, and symbol change history. @@ -25,7 +33,7 @@ export class ExchangesApi { * ``` */ async list(): Promise { - return this.http.get('/exchanges-list/'); + return this.http.get("/exchanges-list/"); } /** @@ -81,6 +89,6 @@ export class ExchangesApi { * ``` */ async symbolChangeHistory(params: DateRange = {}): Promise { - return this.http.get('/symbol-change-history', params); + return this.http.get("/symbol-change-history", params); } } diff --git a/src/api/fundamentals.ts b/src/api/fundamentals.ts index 82f21cd..c13256f 100644 --- a/src/api/fundamentals.ts +++ b/src/api/fundamentals.ts @@ -1,5 +1,11 @@ -import type { HttpClient } from '../http.js'; -import type { Ticker, FundamentalsParams, FundamentalsData, BulkFundamentalsParams, BulkFundamentalsItem } from '../types.js'; +import type { HttpClient } from "../http.js"; +import type { + BulkFundamentalsItem, + BulkFundamentalsParams, + FundamentalsData, + FundamentalsParams, + Ticker, +} from "../types.js"; export class FundamentalsApi { constructor(private http: HttpClient) {} diff --git a/src/api/macro.ts b/src/api/macro.ts index 94632a5..8b9df3a 100644 --- a/src/api/macro.ts +++ b/src/api/macro.ts @@ -1,5 +1,10 @@ -import type { HttpClient } from '../http.js'; -import type { MacroIndicatorParams, MacroIndicatorItem, EconomicEventsParams, EconomicEventsResponse } from '../types.js'; +import type { HttpClient } from "../http.js"; +import type { + EconomicEventsParams, + EconomicEventsResponse, + MacroIndicatorItem, + MacroIndicatorParams, +} from "../types.js"; export class MacroApi { constructor(private http: HttpClient) {} @@ -11,6 +16,6 @@ export class MacroApi { /** Economic events calendar: GET /economic-events */ async economicEvents(params: EconomicEventsParams = {}): Promise { - return this.http.get('/economic-events', params); + return this.http.get("/economic-events", params); } } diff --git a/src/api/news.ts b/src/api/news.ts index d6bd283..907c46a 100644 --- a/src/api/news.ts +++ b/src/api/news.ts @@ -1,21 +1,28 @@ -import type { HttpClient } from '../http.js'; -import type { NewsParams, NewsArticle, SentimentsParams, SentimentItem, NewsWordWeightsParams, NewsWordWeight } from '../types.js'; +import type { HttpClient } from "../http.js"; +import type { + NewsArticle, + NewsParams, + NewsWordWeight, + NewsWordWeightsParams, + SentimentItem, + SentimentsParams, +} from "../types.js"; export class NewsApi { constructor(private http: HttpClient) {} /** Financial news: GET /news */ async news(params: NewsParams = {}): Promise { - return this.http.get('/news', params); + return this.http.get("/news", params); } /** Sentiment data: GET /sentiments */ async sentiments(params: SentimentsParams = {}): Promise> { - return this.http.get('/sentiments', params); + return this.http.get("/sentiments", params); } /** News word weights: GET /news-word-weights */ async newsWordWeights(params: NewsWordWeightsParams = {}): Promise { - return this.http.get('/news-word-weights', params); + return this.http.get("/news-word-weights", params); } } diff --git a/src/api/screening.ts b/src/api/screening.ts index 9587495..6db96e2 100644 --- a/src/api/screening.ts +++ b/src/api/screening.ts @@ -1,12 +1,22 @@ -import type { HttpClient } from '../http.js'; -import type { ScreenerParams, ScreenerResponse, SearchParams, SearchResult, Ticker, IdMappingParams, IdMappingItem, TechnicalParams, TechnicalDataPoint } from '../types.js'; +import type { HttpClient } from "../http.js"; +import type { + IdMappingItem, + IdMappingParams, + ScreenerParams, + ScreenerResponse, + SearchParams, + SearchResult, + TechnicalDataPoint, + TechnicalParams, + Ticker, +} from "../types.js"; export class ScreeningApi { constructor(private http: HttpClient) {} /** Stock market screener: GET /screener */ async screener(params: ScreenerParams = {}): Promise { - return this.http.get('/screener', params); + return this.http.get("/screener", params); } /** Search stocks/ETFs/bonds: GET /search/{query} */ @@ -16,7 +26,7 @@ export class ScreeningApi { /** ID mapping (CUSIP/ISIN/FIGI/LEI/CIK): GET /id-mapping */ async idMapping(params: IdMappingParams = {}): Promise { - return this.http.get('/id-mapping', params); + return this.http.get("/id-mapping", params); } /** Technical indicators: GET /technical/{ticker} */ diff --git a/src/api/treasury.ts b/src/api/treasury.ts index b48e43c..701c173 100644 --- a/src/api/treasury.ts +++ b/src/api/treasury.ts @@ -1,5 +1,5 @@ -import type { HttpClient } from '../http.js'; -import type { TreasuryParams, TreasuryRateItem } from '../types.js'; +import type { HttpClient } from "../http.js"; +import type { TreasuryParams, TreasuryRateItem } from "../types.js"; /** * US Treasury rates API for bill rates, yield curves, long-term rates, and real yields. @@ -26,7 +26,7 @@ export class TreasuryApi { * ``` */ async billRates(params: TreasuryParams = {}): Promise { - return this.http.get('/ust/bill-rates', params); + return this.http.get("/ust/bill-rates", params); } /** @@ -44,7 +44,7 @@ export class TreasuryApi { * ``` */ async yieldRates(params: TreasuryParams = {}): Promise { - return this.http.get('/ust/yield-rates', params); + return this.http.get("/ust/yield-rates", params); } /** @@ -62,7 +62,7 @@ export class TreasuryApi { * ``` */ async longTermRates(params: TreasuryParams = {}): Promise { - return this.http.get('/ust/long-term-rates', params); + return this.http.get("/ust/long-term-rates", params); } /** @@ -80,6 +80,6 @@ export class TreasuryApi { * ``` */ async realYieldRates(params: TreasuryParams = {}): Promise { - return this.http.get('/ust/real-yield-rates', params); + return this.http.get("/ust/real-yield-rates", params); } } diff --git a/src/api/user.ts b/src/api/user.ts index c5a7fa5..ad83d6c 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,11 +1,11 @@ -import type { HttpClient } from '../http.js'; -import type { UserData } from '../types.js'; +import type { HttpClient } from "../http.js"; +import type { UserData } from "../types.js"; export class UserApi { constructor(private http: HttpClient) {} /** User account info: GET /user */ async user(): Promise { - return this.http.get('/user'); + return this.http.get("/user"); } } diff --git a/src/client.ts b/src/client.ts index 32d57e7..369b5ff 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,50 +1,71 @@ -import { HttpClient } from './http.js'; -import { EODHDWebSocket } from './websocket.js'; -import { type Logger, resolveLogger } from './logger.js'; -import { DEFAULT_RETRY } from './retry.js'; -import type { - Ticker, EodParams, EodDataPoint, IntradayParams, IntradayDataPoint, - RealTimeQuote, RealTimeParams, UsQuoteDelayedParams, UsQuoteDelayedResult, - BulkEodParams, BulkEodDataPoint, - DividendDataPoint, SplitDataPoint, HistoricalMarketCapPoint, - TicksParams, TickDataPoint, - FundamentalsParams, FundamentalsData, - BulkFundamentalsParams, BulkFundamentalsItem, - CalendarEarningsParams, CalendarTrendsParams, CalendarIposParams, - CalendarSplitsParams, CalendarDividendsParams, - NewsParams, NewsArticle, SentimentsParams, SentimentItem, - NewsWordWeightsParams, NewsWordWeight, - ExchangeSymbolsParams, ExchangeDetailsParams, Exchange, - MacroIndicatorParams, MacroIndicatorItem, - EconomicEventsParams, EconomicEventsResponse, - TreasuryParams, CboeIndexParams, - ScreenerParams, ScreenerResponse, SearchParams, SearchResult, - IdMappingParams, IdMappingItem, - TechnicalParams, TechnicalDataPoint, - InsiderTransactionsParams, InsiderTransactionItem, - DateRange, - UserData, - WebSocketFeed, WebSocketOptions, -} from './types.js'; - +import { CalendarApi } from "./api/calendar.js"; +import { CboeApi } from "./api/cboe.js"; +import { CorporateApi } from "./api/corporate.js"; // API modules -import { EodApi } from './api/eod.js'; -import { FundamentalsApi } from './api/fundamentals.js'; -import { CalendarApi } from './api/calendar.js'; -import { NewsApi } from './api/news.js'; -import { ExchangesApi } from './api/exchanges.js'; -import { MacroApi } from './api/macro.js'; -import { TreasuryApi } from './api/treasury.js'; -import { CboeApi } from './api/cboe.js'; -import { ScreeningApi } from './api/screening.js'; -import { CorporateApi } from './api/corporate.js'; -import { UserApi } from './api/user.js'; +import { EodApi } from "./api/eod.js"; +import { ExchangesApi } from "./api/exchanges.js"; +import { FundamentalsApi } from "./api/fundamentals.js"; +import { MacroApi } from "./api/macro.js"; +import { NewsApi } from "./api/news.js"; +import { ScreeningApi } from "./api/screening.js"; +import { TreasuryApi } from "./api/treasury.js"; +import { UserApi } from "./api/user.js"; +import { HttpClient } from "./http.js"; +import { type Logger, resolveLogger } from "./logger.js"; +import { InvestVerteApi } from "./marketplace/investverte.js"; +import { PraamsApi } from "./marketplace/praams.js"; +import { TradingHoursApi } from "./marketplace/tradinghours.js"; // Marketplace modules -import { UnicornBayApi } from './marketplace/unicornbay.js'; -import { TradingHoursApi } from './marketplace/tradinghours.js'; -import { PraamsApi } from './marketplace/praams.js'; -import { InvestVerteApi } from './marketplace/investverte.js'; +import { UnicornBayApi } from "./marketplace/unicornbay.js"; +import { DEFAULT_RETRY } from "./retry.js"; +import type { + BulkEodDataPoint, + BulkEodParams, + BulkFundamentalsItem, + BulkFundamentalsParams, + DateRange, + DividendDataPoint, + EconomicEventsParams, + EconomicEventsResponse, + EodDataPoint, + EodParams, + FundamentalsData, + FundamentalsParams, + HistoricalMarketCapPoint, + IdMappingItem, + IdMappingParams, + InsiderTransactionItem, + InsiderTransactionsParams, + IntradayDataPoint, + IntradayParams, + MacroIndicatorItem, + MacroIndicatorParams, + NewsArticle, + NewsParams, + NewsWordWeight, + NewsWordWeightsParams, + RealTimeParams, + RealTimeQuote, + ScreenerParams, + ScreenerResponse, + SearchParams, + SearchResult, + SentimentItem, + SentimentsParams, + SplitDataPoint, + TechnicalDataPoint, + TechnicalParams, + TickDataPoint, + Ticker, + TicksParams, + UserData, + UsQuoteDelayedParams, + UsQuoteDelayedResult, + WebSocketFeed, + WebSocketOptions, +} from "./types.js"; +import { EODHDWebSocket } from "./websocket.js"; export interface EODHDClientOptions { /** Your EODHD API token (falls back to EODHD_API_TOKEN env var) */ @@ -173,20 +194,23 @@ export class EODHDClient { * ``` */ constructor(options: EODHDClientOptions) { - const envToken = typeof process !== 'undefined' ? process.env?.EODHD_API_TOKEN?.trim() : undefined; - const resolved = options.apiToken?.trim() || envToken || ''; + const envToken = typeof process !== "undefined" ? process.env?.EODHD_API_TOKEN?.trim() : undefined; + const resolved = options.apiToken?.trim() || envToken || ""; if (!resolved) { throw new Error( - 'apiToken is required. Pass it to EODHDClient({ apiToken }) or set EODHD_API_TOKEN environment variable.', + "apiToken is required. Pass it to EODHDClient({ apiToken }) or set EODHD_API_TOKEN environment variable.", ); } this.apiToken = resolved; this.http = new HttpClient({ apiToken: resolved, - baseUrl: options.baseUrl ?? 'https://eodhd.com/api/', + baseUrl: options.baseUrl ?? "https://eodhd.com/api/", timeout: options.timeout ?? 30_000, logger: resolveLogger(options.logger), - retryOptions: { ...DEFAULT_RETRY, ...(options.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}) }, + retryOptions: { + ...DEFAULT_RETRY, + ...(options.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}), + }, }); // Core API diff --git a/src/errors.ts b/src/errors.ts index 0f807e9..a641fab 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,28 +1,28 @@ export type ErrorCode = - | 'auth_error' - | 'rate_limit' - | 'network_error' - | 'timeout' - | 'server_error' - | 'client_error' - | 'parse_error' - | 'unknown'; + | "auth_error" + | "rate_limit" + | "network_error" + | "timeout" + | "server_error" + | "client_error" + | "parse_error" + | "unknown"; const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]); export class EODHDError extends Error { - override readonly name: string = 'EODHDError'; + override readonly name: string = "EODHDError"; constructor( message: string, readonly statusCode: number, - readonly code: ErrorCode = 'unknown', + readonly code: ErrorCode = "unknown", readonly requestId?: string, readonly responseBody?: unknown, ) { super(`EODHD API Error (${statusCode}): ${message}`); // Ensure stack trace points to call site, not error constructor - if (typeof Error.captureStackTrace === 'function') { + if (typeof Error.captureStackTrace === "function") { Error.captureStackTrace(this, this.constructor); } } @@ -33,15 +33,15 @@ export class EODHDError extends Error { } export class EODHDAuthError extends EODHDError { - override readonly name = 'EODHDAuthError'; + override readonly name = "EODHDAuthError"; constructor(message: string, statusCode: number = 401, requestId?: string, responseBody?: unknown) { - super(message, statusCode, 'auth_error', requestId, responseBody); + super(message, statusCode, "auth_error", requestId, responseBody); } } export class EODHDRateLimitError extends EODHDError { - override readonly name = 'EODHDRateLimitError'; + override readonly name = "EODHDRateLimitError"; constructor( message: string, @@ -49,15 +49,15 @@ export class EODHDRateLimitError extends EODHDError { requestId?: string, responseBody?: unknown, ) { - super(message, 429, 'rate_limit', requestId, responseBody); + super(message, 429, "rate_limit", requestId, responseBody); } } export class EODHDNetworkError extends EODHDError { - override readonly name = 'EODHDNetworkError'; + override readonly name = "EODHDNetworkError"; constructor(message: string) { - super(message, 0, 'network_error'); + super(message, 0, "network_error"); } override get retryable(): boolean { @@ -66,10 +66,10 @@ export class EODHDNetworkError extends EODHDError { } export class EODHDTimeoutError extends EODHDError { - override readonly name = 'EODHDTimeoutError'; + override readonly name = "EODHDTimeoutError"; constructor(message: string) { - super(message, 0, 'timeout'); + super(message, 0, "timeout"); } override get retryable(): boolean { diff --git a/src/http.ts b/src/http.ts index 285c061..1a0a828 100644 --- a/src/http.ts +++ b/src/http.ts @@ -1,15 +1,9 @@ -import { - EODHDError, - EODHDAuthError, - EODHDRateLimitError, - EODHDNetworkError, - EODHDTimeoutError, -} from './errors.js'; -import { type Logger, NO_OP_LOGGER, redactUrl } from './logger.js'; -import { type RetryOptions, DEFAULT_RETRY, calculateDelay, sleep } from './retry.js'; -import type { RateLimitInfo } from './types.js'; - -export type { ErrorCode } from './errors.js'; +import { EODHDAuthError, EODHDError, EODHDNetworkError, EODHDRateLimitError, EODHDTimeoutError } from "./errors.js"; +import { type Logger, NO_OP_LOGGER, redactUrl } from "./logger.js"; +import { calculateDelay, DEFAULT_RETRY, type RetryOptions, sleep } from "./retry.js"; +import type { RateLimitInfo } from "./types.js"; + +export type { ErrorCode } from "./errors.js"; export { EODHDError, EODHDAuthError, EODHDRateLimitError, EODHDNetworkError, EODHDTimeoutError }; export interface HttpClientConfig { @@ -33,15 +27,15 @@ export class HttpClient { } async get(path: string, params: Params = {}): Promise { - return this.requestWithRetry('GET', path, { ...params, fmt: 'json' }); + return this.requestWithRetry("GET", path, { ...params, fmt: "json" }); } async getBuffer(path: string, params: Params = {}): Promise { - return this.requestWithRetry('GET', path, params, undefined, true); + return this.requestWithRetry("GET", path, params, undefined, true); } async post(path: string, params: Params = {}, body: unknown = {}): Promise { - return this.requestWithRetry('POST', path, { ...params, fmt: 'json' }, body); + return this.requestWithRetry("POST", path, { ...params, fmt: "json" }, body); } private async requestWithRetry( @@ -56,7 +50,7 @@ export class HttpClient { let lastError: Error | undefined; for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) { - this.logger.debug(`${method} ${redacted}${attempt > 0 ? ` (attempt ${attempt + 1})` : ''}`); + this.logger.debug(`${method} ${redacted}${attempt > 0 ? ` (attempt ${attempt + 1})` : ""}`); const start = Date.now(); try { @@ -65,7 +59,7 @@ export class HttpClient { signal: AbortSignal.timeout(this.config.timeout), }; if (body !== undefined) { - fetchOptions.headers = { 'Content-Type': 'application/json' }; + fetchOptions.headers = { "Content-Type": "application/json" }; fetchOptions.body = JSON.stringify(body); } @@ -86,12 +80,12 @@ export class HttpClient { return response.arrayBuffer() as Promise; } try { - return await response.json() as T; + return (await response.json()) as T; } catch (parseErr) { throw new EODHDError( `Failed to parse response as JSON: ${parseErr instanceof Error ? parseErr.message : parseErr}`, response.status, - 'parse_error', + "parse_error", ); } } catch (error) { @@ -118,8 +112,8 @@ export class HttpClient { private buildUrl(path: string, params: Params): URL { // Strip leading slash to properly append to base URL - const cleanPath = path.startsWith('/') ? path.slice(1) : path; - const base = this.config.baseUrl.endsWith('/') ? this.config.baseUrl : this.config.baseUrl + '/'; + const cleanPath = path.startsWith("/") ? path.slice(1) : path; + const base = this.config.baseUrl.endsWith("/") ? this.config.baseUrl : `${this.config.baseUrl}/`; const url = new URL(cleanPath, base); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { @@ -135,32 +129,36 @@ export class HttpClient { try { const text = await response.text(); message = text || response.statusText; - try { responseBody = JSON.parse(text); } catch { responseBody = text || undefined; } + try { + responseBody = JSON.parse(text); + } catch { + responseBody = text || undefined; + } } catch { message = response.statusText; } const status = response.status; - const requestId = response.headers.get('X-Request-Id') ?? undefined; + const requestId = response.headers.get("X-Request-Id") ?? undefined; if (status === 401 || status === 403) { throw new EODHDAuthError(message, status, requestId, responseBody); } if (status === 429) { - const retryAfter = parseRetryAfter(response.headers.get('Retry-After')); + const retryAfter = parseRetryAfter(response.headers.get("Retry-After")); throw new EODHDRateLimitError(message, retryAfter, requestId, responseBody); } - const code = status >= 500 ? 'server_error' : 'client_error'; + const code = status >= 500 ? "server_error" : "client_error"; throw new EODHDError(message, status, code, requestId, responseBody); } } function parseRateLimitHeaders(headers: Headers): RateLimitInfo | undefined { - const limit = headers.get('X-RateLimit-Limit'); - const remaining = headers.get('X-RateLimit-Remaining'); - const reset = headers.get('X-RateLimit-Reset'); + const limit = headers.get("X-RateLimit-Limit"); + const remaining = headers.get("X-RateLimit-Remaining"); + const reset = headers.get("X-RateLimit-Reset"); if (!limit && !remaining && !reset) return undefined; return { limit: limit ? Number(limit) : undefined, @@ -183,15 +181,15 @@ function parseRetryAfter(header: string | null): number | undefined { } function wrapFetchError(err: unknown): EODHDError { - if (err instanceof DOMException && err.name === 'AbortError') { - return new EODHDTimeoutError('Request timed out'); + if (err instanceof DOMException && err.name === "AbortError") { + return new EODHDTimeoutError("Request timed out"); } - if (err instanceof TypeError && err.name === 'TimeoutError') { - return new EODHDTimeoutError('Request timed out'); + if (err instanceof TypeError && err.name === "TimeoutError") { + return new EODHDTimeoutError("Request timed out"); } // Node 18+ AbortSignal.timeout throws with name "TimeoutError" - if (err instanceof DOMException && err.name === 'TimeoutError') { - return new EODHDTimeoutError('Request timed out'); + if (err instanceof DOMException && err.name === "TimeoutError") { + return new EODHDTimeoutError("Request timed out"); } const message = err instanceof Error ? err.message : String(err); return new EODHDNetworkError(message); diff --git a/src/index.ts b/src/index.ts index e1e5d98..81d9dc0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,18 @@ -export { EODHDClient } from './client.js'; -export type { EODHDClientOptions } from './client.js'; +export type { EODHDClientOptions } from "./client.js"; +export { EODHDClient } from "./client.js"; +export type { ErrorCode } from "./errors.js"; export { - EODHDError, EODHDAuthError, - EODHDRateLimitError, + EODHDError, EODHDNetworkError, + EODHDRateLimitError, EODHDTimeoutError, -} from './errors.js'; -export type { ErrorCode } from './errors.js'; -export { EODHDWebSocket } from './websocket.js'; -export type { Logger } from './logger.js'; -export { createConsoleLogger, resolveLogger } from './logger.js'; -export type { RetryOptions } from './retry.js'; -export { DEFAULT_RETRY, calculateDelay } from './retry.js'; -export { paginate } from './pagination.js'; -export type { PaginatedResult, PageFetcher, PaginateOptions } from './pagination.js'; -export * from './types.js'; +} from "./errors.js"; +export type { Logger } from "./logger.js"; +export { createConsoleLogger, resolveLogger } from "./logger.js"; +export type { PageFetcher, PaginatedResult, PaginateOptions } from "./pagination.js"; +export { paginate } from "./pagination.js"; +export type { RetryOptions } from "./retry.js"; +export { calculateDelay, DEFAULT_RETRY } from "./retry.js"; +export * from "./types.js"; +export { EODHDWebSocket } from "./websocket.js"; diff --git a/src/logger.ts b/src/logger.ts index 5cfa1f1..f317817 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -16,10 +16,10 @@ export function createConsoleLogger(): Logger { export function resolveLogger(logger?: Logger): Logger { if (logger) return logger; - if (typeof process !== 'undefined' && process.env?.EODHD_LOG === 'debug') return createConsoleLogger(); + if (typeof process !== "undefined" && process.env?.EODHD_LOG === "debug") return createConsoleLogger(); return NO_OP_LOGGER; } export function redactUrl(url: string): string { - return url.replace(/api_token=[^&]+/, 'api_token=***'); + return url.replace(/api_token=[^&]+/, "api_token=***"); } diff --git a/src/marketplace/investverte.ts b/src/marketplace/investverte.ts index a5f63cb..accba5b 100644 --- a/src/marketplace/investverte.ts +++ b/src/marketplace/investverte.ts @@ -1,22 +1,22 @@ -import type { HttpClient } from '../http.js'; -import type { EsgCompanyParams, EsgCompanyItem, EsgCountryItem, EsgSectorItem, EsgData } from '../types.js'; +import type { HttpClient } from "../http.js"; +import type { EsgCompanyItem, EsgCompanyParams, EsgCountryItem, EsgData, EsgSectorItem } from "../types.js"; export class InvestVerteApi { constructor(private http: HttpClient) {} /** ESG companies list: GET /mp/investverte/companies */ async companies(): Promise { - return this.http.get('/mp/investverte/companies'); + return this.http.get("/mp/investverte/companies"); } /** ESG countries list: GET /mp/investverte/countries */ async countries(): Promise { - return this.http.get('/mp/investverte/countries'); + return this.http.get("/mp/investverte/countries"); } /** ESG sectors list: GET /mp/investverte/sectors */ async sectors(): Promise { - return this.http.get('/mp/investverte/sectors'); + return this.http.get("/mp/investverte/sectors"); } /** Company ESG data: GET /mp/investverte/esg/{symbol} */ diff --git a/src/marketplace/praams.ts b/src/marketplace/praams.ts index ed3c6c5..25df1ee 100644 --- a/src/marketplace/praams.ts +++ b/src/marketplace/praams.ts @@ -1,8 +1,15 @@ -import type { HttpClient } from '../http.js'; +import type { HttpClient } from "../http.js"; import type { - Ticker, PraamsExploreParams, PraamsExploreEquityBody, PraamsExploreBondBody, - PraamsAnalysisResult, PraamsBankStatementResult, PraamsExploreResult, PraamsReportParams, PraamsReportResult, -} from '../types.js'; + PraamsAnalysisResult, + PraamsBankStatementResult, + PraamsExploreBondBody, + PraamsExploreEquityBody, + PraamsExploreParams, + PraamsExploreResult, + PraamsReportParams, + PraamsReportResult, + Ticker, +} from "../types.js"; export class PraamsApi { constructor(private http: HttpClient) {} @@ -49,13 +56,16 @@ export class PraamsApi { // ── Smart Screeners ── /** Smart equity screener: POST /mp/praams/explore/equity */ - async exploreEquity(params: PraamsExploreParams = {}, body: PraamsExploreEquityBody = {}): Promise { - return this.http.post('/mp/praams/explore/equity', params, body); + async exploreEquity( + params: PraamsExploreParams = {}, + body: PraamsExploreEquityBody = {}, + ): Promise { + return this.http.post("/mp/praams/explore/equity", params, body); } /** Smart bond screener: POST /mp/praams/explore/bond */ async exploreBond(params: PraamsExploreParams = {}, body: PraamsExploreBondBody = {}): Promise { - return this.http.post('/mp/praams/explore/bond', params, body); + return this.http.post("/mp/praams/explore/bond", params, body); } // ── Reports ── diff --git a/src/marketplace/tradinghours.ts b/src/marketplace/tradinghours.ts index d0d243b..1367786 100644 --- a/src/marketplace/tradinghours.ts +++ b/src/marketplace/tradinghours.ts @@ -1,29 +1,34 @@ -import type { HttpClient } from '../http.js'; +import type { HttpClient } from "../http.js"; import type { - TradingHoursDetailsParams, TradingHoursLookupParams, TradingHoursStatusParams, - TradingHoursMarket, TradingHoursDetail, TradingHoursLookupResult, TradingHoursStatus, -} from '../types.js'; + TradingHoursDetail, + TradingHoursDetailsParams, + TradingHoursLookupParams, + TradingHoursLookupResult, + TradingHoursMarket, + TradingHoursStatus, + TradingHoursStatusParams, +} from "../types.js"; export class TradingHoursApi { constructor(private http: HttpClient) {} /** Markets list: GET /mp/tradinghours/markets */ async markets(): Promise { - return this.http.get('/mp/tradinghours/markets'); + return this.http.get("/mp/tradinghours/markets"); } /** Market details: GET /mp/tradinghours/markets/details */ async details(params: TradingHoursDetailsParams = {}): Promise { - return this.http.get('/mp/tradinghours/markets/details', params); + return this.http.get("/mp/tradinghours/markets/details", params); } /** Market lookup: GET /mp/tradinghours/markets/lookup */ async lookup(params: TradingHoursLookupParams = {}): Promise { - return this.http.get('/mp/tradinghours/markets/lookup', params); + return this.http.get("/mp/tradinghours/markets/lookup", params); } /** Market status: GET /mp/tradinghours/markets/status */ async status(params: TradingHoursStatusParams = {}): Promise { - return this.http.get('/mp/tradinghours/markets/status', params); + return this.http.get("/mp/tradinghours/markets/status", params); } } diff --git a/src/marketplace/unicornbay.ts b/src/marketplace/unicornbay.ts index bcafe34..353fe8c 100644 --- a/src/marketplace/unicornbay.ts +++ b/src/marketplace/unicornbay.ts @@ -1,29 +1,35 @@ -import type { HttpClient } from '../http.js'; +import type { HttpClient } from "../http.js"; import type { - OptionsContractsParams, OptionsContractsResponse, - OptionsEodParams, OptionsEodResponse, - OptionsUnderlyingSymbolsParams, OptionsUnderlyingSymbolsResponse, - SpGlobalComponentsParams, SpGlobalIndex, SpGlobalComponentsResponse, - MarketplaceTickDataParams, MarketplaceTickDataPoint, + MarketplaceTickDataParams, + MarketplaceTickDataPoint, + OptionsContractsParams, + OptionsContractsResponse, + OptionsEodParams, + OptionsEodResponse, + OptionsUnderlyingSymbolsParams, + OptionsUnderlyingSymbolsResponse, + SpGlobalComponentsParams, + SpGlobalComponentsResponse, + SpGlobalIndex, Ticker, -} from '../types.js'; +} from "../types.js"; export class UnicornBayOptionsApi { constructor(private http: HttpClient) {} /** US Options contracts: GET /mp/unicornbay/options/contracts */ async contracts(params: OptionsContractsParams = {}): Promise { - return this.http.get('/mp/unicornbay/options/contracts', params); + return this.http.get("/mp/unicornbay/options/contracts", params); } /** US Options EOD data: GET /mp/unicornbay/options/eod */ async eod(params: OptionsEodParams = {}): Promise { - return this.http.get('/mp/unicornbay/options/eod', params); + return this.http.get("/mp/unicornbay/options/eod", params); } /** US Options underlying symbols: GET /mp/unicornbay/options/underlying-symbols */ async underlyingSymbols(params: OptionsUnderlyingSymbolsParams = {}): Promise { - return this.http.get('/mp/unicornbay/options/underlying-symbols', params); + return this.http.get("/mp/unicornbay/options/underlying-symbols", params); } } @@ -32,7 +38,7 @@ export class UnicornBaySpGlobalApi { /** S&P Global indices list: GET /mp/unicornbay/spglobal/list */ async list(): Promise { - return this.http.get('/mp/unicornbay/spglobal/list'); + return this.http.get("/mp/unicornbay/spglobal/list"); } /** S&P Global index components: GET /mp/unicornbay/spglobal/comp/{symbol} */ diff --git a/src/pagination.ts b/src/pagination.ts index a3384df..8a8ca21 100644 --- a/src/pagination.ts +++ b/src/pagination.ts @@ -1,8 +1,8 @@ export type PageFetcher = (offset: number, limit: number) => Promise; export interface PaginateOptions { - limit?: number; // default 100 - offset?: number; // default 0 + limit?: number; // default 100 + offset?: number; // default 0 } export interface PaginatedResult extends AsyncIterable { diff --git a/src/retry.ts b/src/retry.ts index ba4bddc..9a0f40b 100644 --- a/src/retry.ts +++ b/src/retry.ts @@ -14,7 +14,7 @@ export const DEFAULT_RETRY: RetryOptions = { /** Full jitter: random(floor, min(cap, base * 2^attempt)), floor = initialDelay/2 (min 100ms) */ export function calculateDelay(attempt: number, options: RetryOptions): number { - const exponential = options.initialDelay * Math.pow(options.multiplier, attempt); + const exponential = options.initialDelay * options.multiplier ** attempt; const capped = Math.min(options.maxDelay, exponential); const floor = Math.max(100, options.initialDelay / 2); return floor + Math.random() * (capped - floor); diff --git a/src/types.ts b/src/types.ts index 52a8b8f..9e1d0ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,9 +26,9 @@ export interface Pagination { // ── EOD ── export interface EodParams extends DateRange { - period?: 'd' | 'w' | 'm'; - order?: 'a' | 'd'; - filter?: 'last_close' | 'last_volume' | string; + period?: "d" | "w" | "m"; + order?: "a" | "d"; + filter?: "last_close" | "last_volume" | string; } export interface EodDataPoint { @@ -44,10 +44,10 @@ export interface EodDataPoint { // ── Intraday ── export interface IntradayParams { - interval?: '1m' | '5m' | '1h'; + interval?: "1m" | "5m" | "1h"; from?: number | string; to?: number | string; - 'split-dt'?: 0 | 1; + "split-dt"?: 0 | 1; } export interface IntradayDataPoint { @@ -85,8 +85,8 @@ export interface RealTimeQuote { export interface UsQuoteDelayedParams { s?: string; - 'page[limit]'?: number; - 'page[offset]'?: number; + "page[limit]"?: number; + "page[offset]"?: number; } export interface UsQuoteDelayedResult { @@ -109,7 +109,7 @@ export interface UsQuoteDelayedResult { export interface BulkEodParams { date?: DateString; - type?: 'eod' | 'splits' | 'dividends'; + type?: "eod" | "splits" | "dividends"; symbols?: string; filter?: string; } @@ -205,16 +205,33 @@ export interface ScreenerResponse { // ── Technical ── export type TechnicalFunction = - | 'sma' | 'ema' | 'wma' | 'macd' | 'rsi' | 'stochastic' | 'stochrsi' - | 'dmi' | 'adx' | 'atr' | 'cci' | 'sar' | 'beta' | 'bbands' - | 'volatility' | 'avgvol' | 'avgvolccy' | 'splitadjusted' | 'slope' | 'stddev'; + | "sma" + | "ema" + | "wma" + | "macd" + | "rsi" + | "stochastic" + | "stochrsi" + | "dmi" + | "adx" + | "atr" + | "cci" + | "sar" + | "beta" + | "bbands" + | "volatility" + | "avgvol" + | "avgvolccy" + | "splitadjusted" + | "slope" + | "stddev"; export interface TechnicalParams extends DateRange { function: TechnicalFunction; period?: number; - order?: 'a' | 'd'; + order?: "a" | "d"; filter?: string; - agg_period?: 'd' | 'w' | 'm'; + agg_period?: "d" | "w" | "m"; fast_kperiod?: number; slow_kperiod?: number; slow_dperiod?: number; @@ -289,7 +306,7 @@ export interface InsiderTransactionItem { export interface SearchParams { limit?: number; exchange?: string; - type?: 'all' | 'stock' | 'etf' | 'fund' | 'bond' | 'index' | 'crypto'; + type?: "all" | "stock" | "etf" | "fund" | "bond" | "index" | "crypto"; } export interface SearchResult { @@ -307,14 +324,14 @@ export interface SearchResult { // ── ID Mapping ── export interface IdMappingParams { - 'filter[symbol]'?: string; - 'filter[isin]'?: string; - 'filter[cusip]'?: string; - 'filter[figi]'?: string; - 'filter[lei]'?: string; - 'filter[cik]'?: string; - 'page[limit]'?: number; - 'page[offset]'?: number; + "filter[symbol]"?: string; + "filter[isin]"?: string; + "filter[cusip]"?: string; + "filter[figi]"?: string; + "filter[lei]"?: string; + "filter[cik]"?: string; + "page[limit]"?: number; + "page[offset]"?: number; } export interface IdMappingItem { @@ -372,9 +389,9 @@ export interface SentimentItem { export interface NewsWordWeightsParams { s?: string; - 'filter[date_from]'?: DateString; - 'filter[date_to]'?: DateString; - 'page[limit]'?: number; + "filter[date_from]"?: DateString; + "filter[date_to]"?: DateString; + "page[limit]"?: number; } export interface NewsWordWeight { @@ -400,12 +417,12 @@ export interface CalendarSplitsParams extends DateRange { } export interface CalendarDividendsParams { - 'filter[symbol]'?: string; - 'filter[date_eq]'?: DateString; - 'filter[date_from]'?: DateString; - 'filter[date_to]'?: DateString; - 'page[limit]'?: number; - 'page[offset]'?: number; + "filter[symbol]"?: string; + "filter[date_eq]"?: DateString; + "filter[date_from]"?: DateString; + "filter[date_to]"?: DateString; + "page[limit]"?: number; + "page[offset]"?: number; } export interface CalendarEarningsItem { @@ -516,7 +533,7 @@ export interface CalendarDividendsData { export interface EconomicEventsParams extends DateRange, Pagination { country?: string; - comparison?: 'mom' | 'qoq' | 'yoy'; + comparison?: "mom" | "qoq" | "yoy"; type?: string; } @@ -620,9 +637,9 @@ export interface TreasuryRateItem { // ── CBOE ── export interface CboeIndexParams { - 'filter[index_code]': string; - 'filter[feed_type]': string; - 'filter[date]': DateString; + "filter[index_code]": string; + "filter[feed_type]": string; + "filter[date]": DateString; } export interface CboeIndexItem { @@ -661,32 +678,32 @@ export interface UserData { // ── Marketplace: Unicorn Bay Options ── export interface OptionsContractsParams { - 'filter[underlying_symbol]'?: string; - 'filter[contract]'?: string; - 'filter[exp_date_eq]'?: DateString; - 'filter[exp_date_from]'?: DateString; - 'filter[exp_date_to]'?: DateString; - 'filter[tradetime_eq]'?: DateString; - 'filter[tradetime_from]'?: DateString; - 'filter[tradetime_to]'?: DateString; - 'filter[type]'?: 'put' | 'call'; - 'filter[strike_eq]'?: number; - 'filter[strike_from]'?: number; - 'filter[strike_to]'?: number; + "filter[underlying_symbol]"?: string; + "filter[contract]"?: string; + "filter[exp_date_eq]"?: DateString; + "filter[exp_date_from]"?: DateString; + "filter[exp_date_to]"?: DateString; + "filter[tradetime_eq]"?: DateString; + "filter[tradetime_from]"?: DateString; + "filter[tradetime_to]"?: DateString; + "filter[type]"?: "put" | "call"; + "filter[strike_eq]"?: number; + "filter[strike_from]"?: number; + "filter[strike_to]"?: number; sort?: string; - 'page[offset]'?: number; - 'page[limit]'?: number; - 'fields[options-contracts]'?: string; + "page[offset]"?: number; + "page[limit]"?: number; + "fields[options-contracts]"?: string; } export interface OptionsEodParams extends OptionsContractsParams { compact?: 0 | 1; - 'fields[options-eod]'?: string; + "fields[options-eod]"?: string; } export interface OptionsUnderlyingSymbolsParams { - 'page[offset]'?: number; - 'page[limit]'?: number; + "page[offset]"?: number; + "page[limit]"?: number; } export interface OptionsContract { @@ -790,15 +807,15 @@ export interface MarketplaceTickDataPoint { // ── Marketplace: Trading Hours ── -export interface TradingHoursMarketsParams {} +export type TradingHoursMarketsParams = {}; export interface TradingHoursDetailsParams { market?: string; } export interface TradingHoursLookupParams { - 'filter[symbol]'?: string; - 'filter[exchange]'?: string; + "filter[symbol]"?: string; + "filter[exchange]"?: string; } export interface TradingHoursStatusParams { @@ -892,7 +909,7 @@ export interface PraamsReportResult { export interface EsgCompanyParams { year?: number; - frequency?: 'FY' | 'Q1' | 'Q2' | 'Q3' | 'Q4'; + frequency?: "FY" | "Q1" | "Q2" | "Q3" | "Q4"; } export interface EsgCompanyItem { @@ -919,7 +936,7 @@ export interface EsgData { // ── WebSocket ── -export type WebSocketFeed = 'us' | 'us-quote' | 'forex' | 'crypto'; +export type WebSocketFeed = "us" | "us-quote" | "forex" | "crypto"; export interface WebSocketOptions { maxReconnectAttempts?: number; diff --git a/src/websocket.ts b/src/websocket.ts index 8cbf542..f24d6af 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -1,11 +1,11 @@ -import { calculateDelay } from './retry.js'; -import { redactUrl } from './logger.js'; -import type { WebSocketFeed, WebSocketOptions, WebSocketTick } from './types.js'; +import { redactUrl } from "./logger.js"; +import { calculateDelay } from "./retry.js"; +import type { WebSocketFeed, WebSocketOptions, WebSocketTick } from "./types.js"; type WebSocketListener = (data: WebSocketTick) => void; type ErrorListener = (error: Error) => void; -const WS_BASE_URL = 'wss://ws.eodhistoricaldata.com/ws'; +const WS_BASE_URL = "wss://ws.eodhistoricaldata.com/ws"; /** * Real-time WebSocket client for streaming market data from EODHD. @@ -94,16 +94,16 @@ export class EODHDWebSocket { * ws.on('reconnectFailed', () => console.log('Reconnect exhausted')); * ``` */ - on(event: 'data', listener: WebSocketListener): this; - on(event: 'error', listener: ErrorListener): this; - on(event: 'close', listener: () => void): this; - on(event: 'reconnectFailed', listener: () => void): this; + on(event: "data", listener: WebSocketListener): this; + on(event: "error", listener: ErrorListener): this; + on(event: "close", listener: () => void): this; + on(event: "reconnectFailed", listener: () => void): this; // eslint-disable-next-line @typescript-eslint/no-explicit-any on(event: string, listener: (...args: any[]) => void): this { - if (event === 'data') this.listeners.push(listener as WebSocketListener); - else if (event === 'error') this.errorListeners.push(listener as ErrorListener); - else if (event === 'close') this.closeListeners.push(listener as () => void); - else if (event === 'reconnectFailed') this.reconnectFailedListeners.push(listener as () => void); + if (event === "data") this.listeners.push(listener as WebSocketListener); + else if (event === "error") this.errorListeners.push(listener as ErrorListener); + else if (event === "close") this.closeListeners.push(listener as () => void); + else if (event === "reconnectFailed") this.reconnectFailedListeners.push(listener as () => void); return this; } @@ -119,7 +119,7 @@ export class EODHDWebSocket { */ subscribe(symbols: string[]): void { if (this.ws && symbols.length > 0) { - this.ws.send(JSON.stringify({ action: 'subscribe', symbols: symbols.join(',') })); + this.ws.send(JSON.stringify({ action: "subscribe", symbols: symbols.join(",") })); } } @@ -135,7 +135,7 @@ export class EODHDWebSocket { */ unsubscribe(symbols: string[]): void { if (this.ws && symbols.length > 0) { - this.ws.send(JSON.stringify({ action: 'unsubscribe', symbols: symbols.join(',') })); + this.ws.send(JSON.stringify({ action: "unsubscribe", symbols: symbols.join(",") })); } } @@ -156,11 +156,11 @@ export class EODHDWebSocket { /** Set up connection — uses dynamic import('ws') in Node.js for ESM compat. */ private initConnection(url: string): void { - if (typeof globalThis.WebSocket !== 'undefined') { + if (typeof globalThis.WebSocket !== "undefined") { this.createConnection(url, globalThis.WebSocket); } else { // Node.js: dynamic import for ESM compatibility - import('ws') + import("ws") .then((mod) => { const WS = mod.default || mod; this.createConnection(url, WS as unknown as typeof WebSocket); @@ -184,28 +184,28 @@ export class EODHDWebSocket { this.reconnecting = false; // Subscribe to symbols if (this.symbols.length > 0) { - const msg = JSON.stringify({ action: 'subscribe', symbols: this.symbols.join(',') }); - this.ws!.send(msg); + const msg = JSON.stringify({ action: "subscribe", symbols: this.symbols.join(",") }); + this.ws?.send(msg); } }; this.ws!.onmessage = (event: MessageEvent) => { let data: unknown; try { - data = JSON.parse(typeof event.data === 'string' ? event.data : event.data.toString()); + data = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString()); } catch (err) { this.emitError(new Error(`WebSocket message parse error: ${err}`)); return; } // Skip status messages - if (data && typeof data === 'object' && 'status_code' in data) return; + if (data && typeof data === "object" && "status_code" in data) return; for (const listener of this.listeners) { listener(data as WebSocketTick); } }; this.ws!.onerror = (event: Event) => { - this.emitError(new Error(`WebSocket error: ${redactUrl((event as ErrorEvent).message || 'unknown')}`)); + this.emitError(new Error(`WebSocket error: ${redactUrl((event as ErrorEvent).message || "unknown")}`)); }; this.ws!.onclose = () => { diff --git a/test/client.test.ts b/test/client.test.ts index 7112932..0a26b90 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { EODHDClient } from '../src/client.js'; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { EODHDClient } from "../src/client.js"; // --------------------------------------------------------------------------- // Helpers @@ -17,14 +17,14 @@ function mockFetchOk(body: unknown = {}) { } function createClient() { - return new EODHDClient({ apiToken: 'test-token', baseUrl: 'https://eodhd.com/api/', timeout: 5000, maxRetries: 0 }); + return new EODHDClient({ apiToken: "test-token", baseUrl: "https://eodhd.com/api/", timeout: 5000, maxRetries: 0 }); } function getCalledUrl(mockFetch: ReturnType): string { return mockFetch.mock.calls[0][0]; } -function getCalledMethod(mockFetch: ReturnType): string { +function _getCalledMethod(mockFetch: ReturnType): string { return mockFetch.mock.calls[0][1].method; } @@ -32,50 +32,50 @@ function getCalledMethod(mockFetch: ReturnType): string { // Constructor // --------------------------------------------------------------------------- -describe('EODHDClient', () => { +describe("EODHDClient", () => { let fetch: ReturnType; beforeEach(() => { fetch = mockFetchOk(); - vi.stubGlobal('fetch', fetch); + vi.stubGlobal("fetch", fetch); }); afterEach(() => { vi.restoreAllMocks(); }); - describe('constructor', () => { - it('creates client with required apiToken', () => { - const client = new EODHDClient({ apiToken: 'abc' }); + describe("constructor", () => { + it("creates client with required apiToken", () => { + const client = new EODHDClient({ apiToken: "abc" }); expect(client).toBeInstanceOf(EODHDClient); }); - it('uses default baseUrl and timeout', async () => { - const client = new EODHDClient({ apiToken: 'abc' }); + it("uses default baseUrl and timeout", async () => { + const client = new EODHDClient({ apiToken: "abc" }); await client.user(); const url = getCalledUrl(fetch); - expect(url).toContain('eodhd.com/api/'); + expect(url).toContain("eodhd.com/api/"); }); - it('accepts custom baseUrl', async () => { - const client = new EODHDClient({ apiToken: 'abc', baseUrl: 'https://custom.api.com/v1/' }); + it("accepts custom baseUrl", async () => { + const client = new EODHDClient({ apiToken: "abc", baseUrl: "https://custom.api.com/v1/" }); await client.user(); const url = getCalledUrl(fetch); - expect(url).toContain('custom.api.com/v1/'); + expect(url).toContain("custom.api.com/v1/"); }); - it('passes apiToken in every request', async () => { - const client = new EODHDClient({ apiToken: 'secret-key' }); + it("passes apiToken in every request", async () => { + const client = new EODHDClient({ apiToken: "secret-key" }); await client.user(); const url = getCalledUrl(fetch); - expect(url).toContain('api_token=secret-key'); + expect(url).toContain("api_token=secret-key"); }); }); - describe('apiToken validation', () => { + describe("apiToken validation", () => { const savedEnv = process.env.EODHD_API_TOKEN; afterEach(() => { @@ -86,93 +86,93 @@ describe('EODHDClient', () => { } }); - it('throws if apiToken is empty string', () => { + it("throws if apiToken is empty string", () => { delete process.env.EODHD_API_TOKEN; - expect(() => new EODHDClient({ apiToken: '' })).toThrow('apiToken is required'); + expect(() => new EODHDClient({ apiToken: "" })).toThrow("apiToken is required"); }); - it('throws if apiToken is whitespace only', () => { + it("throws if apiToken is whitespace only", () => { delete process.env.EODHD_API_TOKEN; - expect(() => new EODHDClient({ apiToken: ' ' })).toThrow('apiToken is required'); + expect(() => new EODHDClient({ apiToken: " " })).toThrow("apiToken is required"); }); - it('reads EODHD_API_TOKEN from env if no apiToken provided', () => { - process.env.EODHD_API_TOKEN = 'env-token'; + it("reads EODHD_API_TOKEN from env if no apiToken provided", () => { + process.env.EODHD_API_TOKEN = "env-token"; const client = new EODHDClient({} as any); expect(client).toBeDefined(); }); - it('throws if neither apiToken nor env var set', () => { + it("throws if neither apiToken nor env var set", () => { delete process.env.EODHD_API_TOKEN; - expect(() => new EODHDClient({} as any)).toThrow('apiToken is required'); + expect(() => new EODHDClient({} as any)).toThrow("apiToken is required"); }); - it('trims apiToken', async () => { - const client = new EODHDClient({ apiToken: ' my-token ' }); + it("trims apiToken", async () => { + const client = new EODHDClient({ apiToken: " my-token " }); await client.user(); const url = getCalledUrl(fetch); - expect(url).toContain('api_token=my-token'); + expect(url).toContain("api_token=my-token"); }); - it('trims env var token', async () => { - process.env.EODHD_API_TOKEN = ' env-trimmed '; + it("trims env var token", async () => { + process.env.EODHD_API_TOKEN = " env-trimmed "; const client = new EODHDClient({} as any); await client.user(); const url = getCalledUrl(fetch); - expect(url).toContain('api_token=env-trimmed'); + expect(url).toContain("api_token=env-trimmed"); }); - it('prefers explicit apiToken over env var', async () => { - process.env.EODHD_API_TOKEN = 'env-token'; - const client = new EODHDClient({ apiToken: 'explicit-token' }); + it("prefers explicit apiToken over env var", async () => { + process.env.EODHD_API_TOKEN = "env-token"; + const client = new EODHDClient({ apiToken: "explicit-token" }); await client.user(); const url = getCalledUrl(fetch); - expect(url).toContain('api_token=explicit-token'); + expect(url).toContain("api_token=explicit-token"); }); }); // ── Exposes sub-modules ───────────────────────────────────────────────── - describe('sub-module exposure', () => { - it('exposes calendar as a direct property', () => { + describe("sub-module exposure", () => { + it("exposes calendar as a direct property", () => { const client = createClient(); expect(client.calendar).toBeDefined(); - expect(typeof client.calendar.earnings).toBe('function'); - expect(typeof client.calendar.trends).toBe('function'); - expect(typeof client.calendar.ipos).toBe('function'); - expect(typeof client.calendar.splits).toBe('function'); - expect(typeof client.calendar.dividends).toBe('function'); + expect(typeof client.calendar.earnings).toBe("function"); + expect(typeof client.calendar.trends).toBe("function"); + expect(typeof client.calendar.ipos).toBe("function"); + expect(typeof client.calendar.splits).toBe("function"); + expect(typeof client.calendar.dividends).toBe("function"); }); - it('exposes exchanges as a direct property', () => { + it("exposes exchanges as a direct property", () => { const client = createClient(); expect(client.exchanges).toBeDefined(); - expect(typeof client.exchanges.list).toBe('function'); - expect(typeof client.exchanges.symbols).toBe('function'); - expect(typeof client.exchanges.details).toBe('function'); - expect(typeof client.exchanges.symbolChangeHistory).toBe('function'); + expect(typeof client.exchanges.list).toBe("function"); + expect(typeof client.exchanges.symbols).toBe("function"); + expect(typeof client.exchanges.details).toBe("function"); + expect(typeof client.exchanges.symbolChangeHistory).toBe("function"); }); - it('exposes treasury as a direct property', () => { + it("exposes treasury as a direct property", () => { const client = createClient(); expect(client.treasury).toBeDefined(); - expect(typeof client.treasury.billRates).toBe('function'); - expect(typeof client.treasury.yieldRates).toBe('function'); - expect(typeof client.treasury.longTermRates).toBe('function'); - expect(typeof client.treasury.realYieldRates).toBe('function'); + expect(typeof client.treasury.billRates).toBe("function"); + expect(typeof client.treasury.yieldRates).toBe("function"); + expect(typeof client.treasury.longTermRates).toBe("function"); + expect(typeof client.treasury.realYieldRates).toBe("function"); }); - it('exposes cboe as a direct property', () => { + it("exposes cboe as a direct property", () => { const client = createClient(); expect(client.cboe).toBeDefined(); - expect(typeof client.cboe.indices).toBe('function'); - expect(typeof client.cboe.index).toBe('function'); + expect(typeof client.cboe.indices).toBe("function"); + expect(typeof client.cboe.index).toBe("function"); }); - it('exposes marketplace with all providers', () => { + it("exposes marketplace with all providers", () => { const client = createClient(); expect(client.marketplace).toBeDefined(); expect(client.marketplace.unicornbay).toBeDefined(); @@ -184,364 +184,363 @@ describe('EODHDClient', () => { // ── Delegation: EOD & Price Data ──────────────────────────────────────── - describe('EOD delegation', () => { - it('eod() calls /eod/{ticker}', async () => { + describe("EOD delegation", () => { + it("eod() calls /eod/{ticker}", async () => { const client = createClient(); - await client.eod('AAPL.US', { from: '2025-01-01' }); + await client.eod("AAPL.US", { from: "2025-01-01" }); const url = getCalledUrl(fetch); - expect(url).toContain('/eod/AAPL.US'); - expect(url).toContain('from=2025-01-01'); + expect(url).toContain("/eod/AAPL.US"); + expect(url).toContain("from=2025-01-01"); }); - it('realTime() calls /real-time/{ticker}', async () => { + it("realTime() calls /real-time/{ticker}", async () => { const client = createClient(); - await client.realTime('MSFT.US'); + await client.realTime("MSFT.US"); - expect(getCalledUrl(fetch)).toContain('/real-time/MSFT.US'); + expect(getCalledUrl(fetch)).toContain("/real-time/MSFT.US"); }); - it('intraday() calls /intraday/{ticker}', async () => { + it("intraday() calls /intraday/{ticker}", async () => { const client = createClient(); - await client.intraday('TSLA.US', { interval: '5m' }); + await client.intraday("TSLA.US", { interval: "5m" }); const url = getCalledUrl(fetch); - expect(url).toContain('/intraday/TSLA.US'); - expect(url).toContain('interval=5m'); + expect(url).toContain("/intraday/TSLA.US"); + expect(url).toContain("interval=5m"); }); - it('usQuoteDelayed() calls /us-quote-delayed', async () => { + it("usQuoteDelayed() calls /us-quote-delayed", async () => { const client = createClient(); - await client.usQuoteDelayed({ s: 'AAPL' }); + await client.usQuoteDelayed({ s: "AAPL" }); const url = getCalledUrl(fetch); - expect(url).toContain('/us-quote-delayed'); - expect(url).toContain('s=AAPL'); + expect(url).toContain("/us-quote-delayed"); + expect(url).toContain("s=AAPL"); }); - it('bulkEod() calls /eod-bulk-last-day/{exchange}', async () => { + it("bulkEod() calls /eod-bulk-last-day/{exchange}", async () => { const client = createClient(); - await client.bulkEod('US', { date: '2025-01-15' }); + await client.bulkEod("US", { date: "2025-01-15" }); const url = getCalledUrl(fetch); - expect(url).toContain('/eod-bulk-last-day/US'); - expect(url).toContain('date=2025-01-15'); + expect(url).toContain("/eod-bulk-last-day/US"); + expect(url).toContain("date=2025-01-15"); }); - it('dividends() calls /div/{ticker}', async () => { + it("dividends() calls /div/{ticker}", async () => { const client = createClient(); - await client.dividends('AAPL.US', { from: '2024-01-01' }); + await client.dividends("AAPL.US", { from: "2024-01-01" }); const url = getCalledUrl(fetch); - expect(url).toContain('/div/AAPL.US'); - expect(url).toContain('from=2024-01-01'); + expect(url).toContain("/div/AAPL.US"); + expect(url).toContain("from=2024-01-01"); }); - it('splits() calls /splits/{ticker}', async () => { + it("splits() calls /splits/{ticker}", async () => { const client = createClient(); - await client.splits('AAPL.US'); + await client.splits("AAPL.US"); - expect(getCalledUrl(fetch)).toContain('/splits/AAPL.US'); + expect(getCalledUrl(fetch)).toContain("/splits/AAPL.US"); }); - it('historicalMarketCap() calls /historical-market-cap/{ticker}', async () => { + it("historicalMarketCap() calls /historical-market-cap/{ticker}", async () => { const client = createClient(); - await client.historicalMarketCap('AAPL.US'); + await client.historicalMarketCap("AAPL.US"); - expect(getCalledUrl(fetch)).toContain('/historical-market-cap/AAPL.US'); + expect(getCalledUrl(fetch)).toContain("/historical-market-cap/AAPL.US"); }); - it('ticks() calls /ticks', async () => { + it("ticks() calls /ticks", async () => { const client = createClient(); - await client.ticks({ s: 'AAPL', limit: 10 }); + await client.ticks({ s: "AAPL", limit: 10 }); const url = getCalledUrl(fetch); - expect(url).toContain('/ticks'); - expect(url).toContain('s=AAPL'); - expect(url).toContain('limit=10'); + expect(url).toContain("/ticks"); + expect(url).toContain("s=AAPL"); + expect(url).toContain("limit=10"); }); }); // ── Delegation: Fundamentals ──────────────────────────────────────────── - describe('Fundamentals delegation', () => { - it('fundamentals() calls /fundamentals/{ticker}', async () => { + describe("Fundamentals delegation", () => { + it("fundamentals() calls /fundamentals/{ticker}", async () => { const client = createClient(); - await client.fundamentals('AAPL.US', { filter: 'General' }); + await client.fundamentals("AAPL.US", { filter: "General" }); const url = getCalledUrl(fetch); - expect(url).toContain('/fundamentals/AAPL.US'); - expect(url).toContain('filter=General'); + expect(url).toContain("/fundamentals/AAPL.US"); + expect(url).toContain("filter=General"); }); - it('bulkFundamentals() calls /bulk-fundamentals/{exchange}', async () => { + it("bulkFundamentals() calls /bulk-fundamentals/{exchange}", async () => { const client = createClient(); - await client.bulkFundamentals('US', { symbols: 'AAPL,MSFT' }); + await client.bulkFundamentals("US", { symbols: "AAPL,MSFT" }); const url = getCalledUrl(fetch); - expect(url).toContain('/bulk-fundamentals/US'); - expect(url).toContain('symbols=AAPL%2CMSFT'); + expect(url).toContain("/bulk-fundamentals/US"); + expect(url).toContain("symbols=AAPL%2CMSFT"); }); - -}); + }); // ── Delegation: News & Sentiment ──────────────────────────────────────── - describe('News delegation', () => { - it('news() calls /news', async () => { + describe("News delegation", () => { + it("news() calls /news", async () => { const client = createClient(); - await client.news({ s: 'AAPL.US', limit: 5 }); + await client.news({ s: "AAPL.US", limit: 5 }); const url = getCalledUrl(fetch); - expect(url).toContain('/news'); - expect(url).toContain('s=AAPL.US'); - expect(url).toContain('limit=5'); + expect(url).toContain("/news"); + expect(url).toContain("s=AAPL.US"); + expect(url).toContain("limit=5"); }); - it('sentiments() calls /sentiments', async () => { + it("sentiments() calls /sentiments", async () => { const client = createClient(); - await client.sentiments({ s: 'AAPL.US' }); + await client.sentiments({ s: "AAPL.US" }); const url = getCalledUrl(fetch); - expect(url).toContain('/sentiments'); - expect(url).toContain('s=AAPL.US'); + expect(url).toContain("/sentiments"); + expect(url).toContain("s=AAPL.US"); }); - it('newsWordWeights() calls /news-word-weights', async () => { + it("newsWordWeights() calls /news-word-weights", async () => { const client = createClient(); - await client.newsWordWeights({ s: 'AAPL.US' }); + await client.newsWordWeights({ s: "AAPL.US" }); const url = getCalledUrl(fetch); - expect(url).toContain('/news-word-weights'); + expect(url).toContain("/news-word-weights"); }); }); // ── Delegation: Screening & Search ────────────────────────────────────── - describe('Screening delegation', () => { - it('screener() calls /screener', async () => { + describe("Screening delegation", () => { + it("screener() calls /screener", async () => { const client = createClient(); - await client.screener({ filters: 'market_capitalization>100000000000' }); + await client.screener({ filters: "market_capitalization>100000000000" }); const url = getCalledUrl(fetch); - expect(url).toContain('/screener'); + expect(url).toContain("/screener"); }); - it('search() calls /search/{query}', async () => { + it("search() calls /search/{query}", async () => { const client = createClient(); - await client.search('Apple', { limit: 10, type: 'stock' }); + await client.search("Apple", { limit: 10, type: "stock" }); const url = getCalledUrl(fetch); - expect(url).toContain('/search/Apple'); - expect(url).toContain('limit=10'); - expect(url).toContain('type=stock'); + expect(url).toContain("/search/Apple"); + expect(url).toContain("limit=10"); + expect(url).toContain("type=stock"); }); - it('idMapping() calls /id-mapping', async () => { + it("idMapping() calls /id-mapping", async () => { const client = createClient(); - await client.idMapping({ 'filter[isin]': 'US0378331005' }); + await client.idMapping({ "filter[isin]": "US0378331005" }); const url = getCalledUrl(fetch); - expect(url).toContain('/id-mapping'); + expect(url).toContain("/id-mapping"); }); - it('technical() calls /technical/{ticker}', async () => { + it("technical() calls /technical/{ticker}", async () => { const client = createClient(); - await client.technical('AAPL.US', { function: 'sma', period: 50 }); + await client.technical("AAPL.US", { function: "sma", period: 50 }); const url = getCalledUrl(fetch); - expect(url).toContain('/technical/AAPL.US'); - expect(url).toContain('function=sma'); - expect(url).toContain('period=50'); + expect(url).toContain("/technical/AAPL.US"); + expect(url).toContain("function=sma"); + expect(url).toContain("period=50"); }); }); // ── Delegation: Corporate ─────────────────────────────────────────────── - describe('Corporate delegation', () => { - it('insiderTransactions() calls /insider-transactions', async () => { + describe("Corporate delegation", () => { + it("insiderTransactions() calls /insider-transactions", async () => { const client = createClient(); - await client.insiderTransactions({ code: 'AAPL' }); + await client.insiderTransactions({ code: "AAPL" }); const url = getCalledUrl(fetch); - expect(url).toContain('/insider-transactions'); - expect(url).toContain('code=AAPL'); + expect(url).toContain("/insider-transactions"); + expect(url).toContain("code=AAPL"); }); }); // ── Delegation: Macro ─────────────────────────────────────────────────── - describe('Macro delegation', () => { - it('macroIndicator() calls /macro-indicator/{country}', async () => { + describe("Macro delegation", () => { + it("macroIndicator() calls /macro-indicator/{country}", async () => { const client = createClient(); - await client.macroIndicator('USA', { indicator: 'gdp_current_usd' }); + await client.macroIndicator("USA", { indicator: "gdp_current_usd" }); const url = getCalledUrl(fetch); - expect(url).toContain('/macro-indicator/USA'); - expect(url).toContain('indicator=gdp_current_usd'); + expect(url).toContain("/macro-indicator/USA"); + expect(url).toContain("indicator=gdp_current_usd"); }); - it('economicEvents() calls /economic-events', async () => { + it("economicEvents() calls /economic-events", async () => { const client = createClient(); - await client.economicEvents({ country: 'US', from: '2025-01-01' }); + await client.economicEvents({ country: "US", from: "2025-01-01" }); const url = getCalledUrl(fetch); - expect(url).toContain('/economic-events'); - expect(url).toContain('country=US'); - expect(url).toContain('from=2025-01-01'); + expect(url).toContain("/economic-events"); + expect(url).toContain("country=US"); + expect(url).toContain("from=2025-01-01"); }); }); // ── Delegation: User ──────────────────────────────────────────────────── - describe('User delegation', () => { - it('user() calls /user', async () => { + describe("User delegation", () => { + it("user() calls /user", async () => { const client = createClient(); await client.user(); - expect(getCalledUrl(fetch)).toContain('/user'); + expect(getCalledUrl(fetch)).toContain("/user"); }); - it('logo() calls /logo/{symbol}', async () => { + it("logo() calls /logo/{symbol}", async () => { const client = createClient(); - await client.logo('AAPL.US'); + await client.logo("AAPL.US"); - expect(getCalledUrl(fetch)).toContain('/logo/AAPL.US'); + expect(getCalledUrl(fetch)).toContain("/logo/AAPL.US"); }); - it('logoSvg() calls /logo-svg/{symbol}', async () => { + it("logoSvg() calls /logo-svg/{symbol}", async () => { const client = createClient(); - await client.logoSvg('AAPL.US'); + await client.logoSvg("AAPL.US"); - expect(getCalledUrl(fetch)).toContain('/logo-svg/AAPL.US'); + expect(getCalledUrl(fetch)).toContain("/logo-svg/AAPL.US"); }); }); // ── Calendar (direct property) ────────────────────────────────────────── - describe('Calendar (via direct property)', () => { - it('calendar.earnings() calls /calendar/earnings', async () => { + describe("Calendar (via direct property)", () => { + it("calendar.earnings() calls /calendar/earnings", async () => { const client = createClient(); - await client.calendar.earnings({ symbols: 'AAPL.US' }); + await client.calendar.earnings({ symbols: "AAPL.US" }); const url = getCalledUrl(fetch); - expect(url).toContain('/calendar/earnings'); - expect(url).toContain('symbols=AAPL.US'); + expect(url).toContain("/calendar/earnings"); + expect(url).toContain("symbols=AAPL.US"); }); - it('calendar.trends() calls /calendar/trends', async () => { + it("calendar.trends() calls /calendar/trends", async () => { const client = createClient(); - await client.calendar.trends({ symbols: 'AAPL.US' }); + await client.calendar.trends({ symbols: "AAPL.US" }); - expect(getCalledUrl(fetch)).toContain('/calendar/trends'); + expect(getCalledUrl(fetch)).toContain("/calendar/trends"); }); - it('calendar.ipos() calls /calendar/ipos', async () => { + it("calendar.ipos() calls /calendar/ipos", async () => { const client = createClient(); await client.calendar.ipos(); - expect(getCalledUrl(fetch)).toContain('/calendar/ipos'); + expect(getCalledUrl(fetch)).toContain("/calendar/ipos"); }); - it('calendar.splits() calls /calendar/splits', async () => { + it("calendar.splits() calls /calendar/splits", async () => { const client = createClient(); await client.calendar.splits(); - expect(getCalledUrl(fetch)).toContain('/calendar/splits'); + expect(getCalledUrl(fetch)).toContain("/calendar/splits"); }); - it('calendar.dividends() calls /calendar/dividends', async () => { + it("calendar.dividends() calls /calendar/dividends", async () => { const client = createClient(); await client.calendar.dividends(); - expect(getCalledUrl(fetch)).toContain('/calendar/dividends'); + expect(getCalledUrl(fetch)).toContain("/calendar/dividends"); }); }); // ── Exchanges (direct property) ───────────────────────────────────────── - describe('Exchanges (via direct property)', () => { - it('exchanges.list() calls /exchanges-list/', async () => { + describe("Exchanges (via direct property)", () => { + it("exchanges.list() calls /exchanges-list/", async () => { const client = createClient(); await client.exchanges.list(); - expect(getCalledUrl(fetch)).toContain('/exchanges-list/'); + expect(getCalledUrl(fetch)).toContain("/exchanges-list/"); }); - it('exchanges.symbols() calls /exchange-symbol-list/{code}', async () => { + it("exchanges.symbols() calls /exchange-symbol-list/{code}", async () => { const client = createClient(); - await client.exchanges.symbols('US'); + await client.exchanges.symbols("US"); - expect(getCalledUrl(fetch)).toContain('/exchange-symbol-list/US'); + expect(getCalledUrl(fetch)).toContain("/exchange-symbol-list/US"); }); - it('exchanges.details() calls /exchange-details/{code}', async () => { + it("exchanges.details() calls /exchange-details/{code}", async () => { const client = createClient(); - await client.exchanges.details('US'); + await client.exchanges.details("US"); - expect(getCalledUrl(fetch)).toContain('/exchange-details/US'); + expect(getCalledUrl(fetch)).toContain("/exchange-details/US"); }); - it('exchanges.symbolChangeHistory() calls /symbol-change-history', async () => { + it("exchanges.symbolChangeHistory() calls /symbol-change-history", async () => { const client = createClient(); await client.exchanges.symbolChangeHistory(); - expect(getCalledUrl(fetch)).toContain('/symbol-change-history'); + expect(getCalledUrl(fetch)).toContain("/symbol-change-history"); }); }); // ── Treasury (direct property) ────────────────────────────────────────── - describe('Treasury (via direct property)', () => { - it('treasury.billRates() calls /ust/bill-rates', async () => { + describe("Treasury (via direct property)", () => { + it("treasury.billRates() calls /ust/bill-rates", async () => { const client = createClient(); await client.treasury.billRates(); - expect(getCalledUrl(fetch)).toContain('/ust/bill-rates'); + expect(getCalledUrl(fetch)).toContain("/ust/bill-rates"); }); - it('treasury.yieldRates() calls /ust/yield-rates', async () => { + it("treasury.yieldRates() calls /ust/yield-rates", async () => { const client = createClient(); await client.treasury.yieldRates(); - expect(getCalledUrl(fetch)).toContain('/ust/yield-rates'); + expect(getCalledUrl(fetch)).toContain("/ust/yield-rates"); }); - it('treasury.longTermRates() calls /ust/long-term-rates', async () => { + it("treasury.longTermRates() calls /ust/long-term-rates", async () => { const client = createClient(); await client.treasury.longTermRates(); - expect(getCalledUrl(fetch)).toContain('/ust/long-term-rates'); + expect(getCalledUrl(fetch)).toContain("/ust/long-term-rates"); }); - it('treasury.realYieldRates() calls /ust/real-yield-rates', async () => { + it("treasury.realYieldRates() calls /ust/real-yield-rates", async () => { const client = createClient(); await client.treasury.realYieldRates(); - expect(getCalledUrl(fetch)).toContain('/ust/real-yield-rates'); + expect(getCalledUrl(fetch)).toContain("/ust/real-yield-rates"); }); }); // ── CBOE (direct property) ────────────────────────────────────────────── - describe('CBOE (via direct property)', () => { - it('cboe.indices() calls /cboe/indices', async () => { + describe("CBOE (via direct property)", () => { + it("cboe.indices() calls /cboe/indices", async () => { const client = createClient(); await client.cboe.indices(); - expect(getCalledUrl(fetch)).toContain('/cboe/indices'); + expect(getCalledUrl(fetch)).toContain("/cboe/indices"); }); - it('cboe.index() calls /cboe/index', async () => { + it("cboe.index() calls /cboe/index", async () => { const client = createClient(); await client.cboe.index({ - 'filter[index_code]': 'BUK100P', - 'filter[feed_type]': 'eod', - 'filter[date]': '2025-01-15', + "filter[index_code]": "BUK100P", + "filter[feed_type]": "eod", + "filter[date]": "2025-01-15", }); const url = getCalledUrl(fetch); - expect(url).toContain('/cboe/index'); + expect(url).toContain("/cboe/index"); }); }); }); diff --git a/test/errors.test.ts b/test/errors.test.ts index 968581c..41a1640 100644 --- a/test/errors.test.ts +++ b/test/errors.test.ts @@ -1,101 +1,101 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from "vitest"; import { - EODHDError, EODHDAuthError, - EODHDRateLimitError, + EODHDError, EODHDNetworkError, + EODHDRateLimitError, EODHDTimeoutError, -} from '../src/errors.js'; +} from "../src/errors.js"; // --------------------------------------------------------------------------- // EODHDError // --------------------------------------------------------------------------- -describe('EODHDError', () => { - it('has correct name', () => { - const err = new EODHDError('bad request', 400); - expect(err.name).toBe('EODHDError'); +describe("EODHDError", () => { + it("has correct name", () => { + const err = new EODHDError("bad request", 400); + expect(err.name).toBe("EODHDError"); }); - it('stores statusCode', () => { - const err = new EODHDError('not found', 404); + it("stores statusCode", () => { + const err = new EODHDError("not found", 404); expect(err.statusCode).toBe(404); }); - it('defaults code to unknown', () => { - const err = new EODHDError('fail', 500); - expect(err.code).toBe('unknown'); + it("defaults code to unknown", () => { + const err = new EODHDError("fail", 500); + expect(err.code).toBe("unknown"); }); - it('accepts explicit code', () => { - const err = new EODHDError('fail', 500, 'server_error'); - expect(err.code).toBe('server_error'); + it("accepts explicit code", () => { + const err = new EODHDError("fail", 500, "server_error"); + expect(err.code).toBe("server_error"); }); - it('stores requestId', () => { - const err = new EODHDError('fail', 500, 'server_error', 'req-123'); - expect(err.requestId).toBe('req-123'); + it("stores requestId", () => { + const err = new EODHDError("fail", 500, "server_error", "req-123"); + expect(err.requestId).toBe("req-123"); }); - it('stores responseBody', () => { - const body = { error: 'something broke' }; - const err = new EODHDError('fail', 500, 'server_error', undefined, body); + it("stores responseBody", () => { + const body = { error: "something broke" }; + const err = new EODHDError("fail", 500, "server_error", undefined, body); expect(err.responseBody).toEqual(body); }); - it('includes status code in message', () => { - const err = new EODHDError('Unauthorized', 401); - expect(err.message).toContain('401'); - expect(err.message).toContain('Unauthorized'); + it("includes status code in message", () => { + const err = new EODHDError("Unauthorized", 401); + expect(err.message).toContain("401"); + expect(err.message).toContain("Unauthorized"); }); - it('is instanceof Error', () => { - const err = new EODHDError('test', 500); + it("is instanceof Error", () => { + const err = new EODHDError("test", 500); expect(err).toBeInstanceOf(Error); }); - it('retryable = true for 429', () => { - const err = new EODHDError('rate limited', 429); + it("retryable = true for 429", () => { + const err = new EODHDError("rate limited", 429); expect(err.retryable).toBe(true); }); - it('retryable = true for 500', () => { - const err = new EODHDError('server error', 500); + it("retryable = true for 500", () => { + const err = new EODHDError("server error", 500); expect(err.retryable).toBe(true); }); - it('retryable = true for 502', () => { - const err = new EODHDError('bad gateway', 502); + it("retryable = true for 502", () => { + const err = new EODHDError("bad gateway", 502); expect(err.retryable).toBe(true); }); - it('retryable = true for 503', () => { - const err = new EODHDError('unavailable', 503); + it("retryable = true for 503", () => { + const err = new EODHDError("unavailable", 503); expect(err.retryable).toBe(true); }); - it('retryable = true for 504', () => { - const err = new EODHDError('timeout', 504); + it("retryable = true for 504", () => { + const err = new EODHDError("timeout", 504); expect(err.retryable).toBe(true); }); - it('retryable = true for 408', () => { - const err = new EODHDError('request timeout', 408); + it("retryable = true for 408", () => { + const err = new EODHDError("request timeout", 408); expect(err.retryable).toBe(true); }); - it('retryable = false for 400', () => { - const err = new EODHDError('bad request', 400); + it("retryable = false for 400", () => { + const err = new EODHDError("bad request", 400); expect(err.retryable).toBe(false); }); - it('retryable = false for 401', () => { - const err = new EODHDError('unauthorized', 401); + it("retryable = false for 401", () => { + const err = new EODHDError("unauthorized", 401); expect(err.retryable).toBe(false); }); - it('retryable = false for 404', () => { - const err = new EODHDError('not found', 404); + it("retryable = false for 404", () => { + const err = new EODHDError("not found", 404); expect(err.retryable).toBe(false); }); }); @@ -104,43 +104,43 @@ describe('EODHDError', () => { // EODHDAuthError // --------------------------------------------------------------------------- -describe('EODHDAuthError', () => { - it('has correct name', () => { - const err = new EODHDAuthError('invalid token'); - expect(err.name).toBe('EODHDAuthError'); +describe("EODHDAuthError", () => { + it("has correct name", () => { + const err = new EODHDAuthError("invalid token"); + expect(err.name).toBe("EODHDAuthError"); }); - it('extends EODHDError', () => { - const err = new EODHDAuthError('invalid token'); + it("extends EODHDError", () => { + const err = new EODHDAuthError("invalid token"); expect(err).toBeInstanceOf(EODHDError); expect(err).toBeInstanceOf(Error); }); - it('has statusCode 401', () => { - const err = new EODHDAuthError('invalid token'); + it("has statusCode 401", () => { + const err = new EODHDAuthError("invalid token"); expect(err.statusCode).toBe(401); }); - it('has code auth_error', () => { - const err = new EODHDAuthError('invalid token'); - expect(err.code).toBe('auth_error'); + it("has code auth_error", () => { + const err = new EODHDAuthError("invalid token"); + expect(err.code).toBe("auth_error"); }); - it('stores requestId and responseBody', () => { - const err = new EODHDAuthError('invalid token', 401, 'req-456', { error: 'bad key' }); - expect(err.requestId).toBe('req-456'); - expect(err.responseBody).toEqual({ error: 'bad key' }); + it("stores requestId and responseBody", () => { + const err = new EODHDAuthError("invalid token", 401, "req-456", { error: "bad key" }); + expect(err.requestId).toBe("req-456"); + expect(err.responseBody).toEqual({ error: "bad key" }); }); - it('retryable = false', () => { - const err = new EODHDAuthError('invalid token'); + it("retryable = false", () => { + const err = new EODHDAuthError("invalid token"); expect(err.retryable).toBe(false); }); - it('accepts custom status code (e.g. 403)', () => { - const err = new EODHDAuthError('forbidden', 403); + it("accepts custom status code (e.g. 403)", () => { + const err = new EODHDAuthError("forbidden", 403); expect(err.statusCode).toBe(403); - expect(err.code).toBe('auth_error'); + expect(err.code).toBe("auth_error"); }); }); @@ -148,46 +148,46 @@ describe('EODHDAuthError', () => { // EODHDRateLimitError // --------------------------------------------------------------------------- -describe('EODHDRateLimitError', () => { - it('has correct name', () => { - const err = new EODHDRateLimitError('too many requests'); - expect(err.name).toBe('EODHDRateLimitError'); +describe("EODHDRateLimitError", () => { + it("has correct name", () => { + const err = new EODHDRateLimitError("too many requests"); + expect(err.name).toBe("EODHDRateLimitError"); }); - it('extends EODHDError', () => { - const err = new EODHDRateLimitError('too many requests'); + it("extends EODHDError", () => { + const err = new EODHDRateLimitError("too many requests"); expect(err).toBeInstanceOf(EODHDError); expect(err).toBeInstanceOf(Error); }); - it('has statusCode 429', () => { - const err = new EODHDRateLimitError('too many requests'); + it("has statusCode 429", () => { + const err = new EODHDRateLimitError("too many requests"); expect(err.statusCode).toBe(429); }); - it('has code rate_limit', () => { - const err = new EODHDRateLimitError('too many requests'); - expect(err.code).toBe('rate_limit'); + it("has code rate_limit", () => { + const err = new EODHDRateLimitError("too many requests"); + expect(err.code).toBe("rate_limit"); }); - it('stores retryAfter', () => { - const err = new EODHDRateLimitError('too many requests', 30); + it("stores retryAfter", () => { + const err = new EODHDRateLimitError("too many requests", 30); expect(err.retryAfter).toBe(30); }); - it('retryAfter defaults to undefined', () => { - const err = new EODHDRateLimitError('too many requests'); + it("retryAfter defaults to undefined", () => { + const err = new EODHDRateLimitError("too many requests"); expect(err.retryAfter).toBeUndefined(); }); - it('stores requestId and responseBody', () => { - const err = new EODHDRateLimitError('limit', 60, 'req-789', { wait: 60 }); - expect(err.requestId).toBe('req-789'); + it("stores requestId and responseBody", () => { + const err = new EODHDRateLimitError("limit", 60, "req-789", { wait: 60 }); + expect(err.requestId).toBe("req-789"); expect(err.responseBody).toEqual({ wait: 60 }); }); - it('retryable = true', () => { - const err = new EODHDRateLimitError('too many requests'); + it("retryable = true", () => { + const err = new EODHDRateLimitError("too many requests"); expect(err.retryable).toBe(true); }); }); @@ -196,30 +196,30 @@ describe('EODHDRateLimitError', () => { // EODHDNetworkError // --------------------------------------------------------------------------- -describe('EODHDNetworkError', () => { - it('has correct name', () => { - const err = new EODHDNetworkError('connection refused'); - expect(err.name).toBe('EODHDNetworkError'); +describe("EODHDNetworkError", () => { + it("has correct name", () => { + const err = new EODHDNetworkError("connection refused"); + expect(err.name).toBe("EODHDNetworkError"); }); - it('extends EODHDError', () => { - const err = new EODHDNetworkError('connection refused'); + it("extends EODHDError", () => { + const err = new EODHDNetworkError("connection refused"); expect(err).toBeInstanceOf(EODHDError); expect(err).toBeInstanceOf(Error); }); - it('has statusCode 0', () => { - const err = new EODHDNetworkError('connection refused'); + it("has statusCode 0", () => { + const err = new EODHDNetworkError("connection refused"); expect(err.statusCode).toBe(0); }); - it('has code network_error', () => { - const err = new EODHDNetworkError('connection refused'); - expect(err.code).toBe('network_error'); + it("has code network_error", () => { + const err = new EODHDNetworkError("connection refused"); + expect(err.code).toBe("network_error"); }); - it('retryable = true', () => { - const err = new EODHDNetworkError('connection refused'); + it("retryable = true", () => { + const err = new EODHDNetworkError("connection refused"); expect(err.retryable).toBe(true); }); }); @@ -228,30 +228,30 @@ describe('EODHDNetworkError', () => { // EODHDTimeoutError // --------------------------------------------------------------------------- -describe('EODHDTimeoutError', () => { - it('has correct name', () => { - const err = new EODHDTimeoutError('request timed out'); - expect(err.name).toBe('EODHDTimeoutError'); +describe("EODHDTimeoutError", () => { + it("has correct name", () => { + const err = new EODHDTimeoutError("request timed out"); + expect(err.name).toBe("EODHDTimeoutError"); }); - it('extends EODHDError', () => { - const err = new EODHDTimeoutError('request timed out'); + it("extends EODHDError", () => { + const err = new EODHDTimeoutError("request timed out"); expect(err).toBeInstanceOf(EODHDError); expect(err).toBeInstanceOf(Error); }); - it('has statusCode 0', () => { - const err = new EODHDTimeoutError('request timed out'); + it("has statusCode 0", () => { + const err = new EODHDTimeoutError("request timed out"); expect(err.statusCode).toBe(0); }); - it('has code timeout', () => { - const err = new EODHDTimeoutError('request timed out'); - expect(err.code).toBe('timeout'); + it("has code timeout", () => { + const err = new EODHDTimeoutError("request timed out"); + expect(err.code).toBe("timeout"); }); - it('retryable = true', () => { - const err = new EODHDTimeoutError('request timed out'); + it("retryable = true", () => { + const err = new EODHDTimeoutError("request timed out"); expect(err.retryable).toBe(true); }); }); diff --git a/test/http.test.ts b/test/http.test.ts index a74c2d7..d8171ce 100644 --- a/test/http.test.ts +++ b/test/http.test.ts @@ -1,12 +1,12 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { HttpClient } from '../src/http.js'; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { - EODHDError, EODHDAuthError, - EODHDRateLimitError, + EODHDError, EODHDNetworkError, + EODHDRateLimitError, EODHDTimeoutError, -} from '../src/errors.js'; +} from "../src/errors.js"; +import { HttpClient } from "../src/http.js"; // --------------------------------------------------------------------------- // Helpers @@ -14,8 +14,8 @@ import { function createHttpClient(overrides: Partial<{ apiToken: string; baseUrl: string; timeout: number }> = {}) { return new HttpClient({ - apiToken: overrides.apiToken ?? 'demo', - baseUrl: overrides.baseUrl ?? 'https://eodhd.com/api/', + apiToken: overrides.apiToken ?? "demo", + baseUrl: overrides.baseUrl ?? "https://eodhd.com/api/", timeout: overrides.timeout ?? 5000, retryOptions: { maxRetries: 0, initialDelay: 1000, maxDelay: 30000, multiplier: 2 }, }); @@ -25,7 +25,7 @@ function jsonResponse(body: unknown, status = 200) { return { ok: status >= 200 && status < 300, status, - statusText: status === 200 ? 'OK' : 'Error', + statusText: status === 200 ? "OK" : "Error", json: () => Promise.resolve(body), text: () => Promise.resolve(JSON.stringify(body)), arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)), @@ -38,10 +38,10 @@ function errorResponse(status: number, body: string, headers: Record Promise.reject(new Error('not json')), + statusText: "Error", + json: () => Promise.reject(new Error("not json")), text: () => Promise.resolve(body), - arrayBuffer: () => Promise.reject(new Error('not buffer')), + arrayBuffer: () => Promise.reject(new Error("not buffer")), headers: { get: (name: string) => headerMap.get(name) ?? null }, }; } @@ -50,12 +50,12 @@ function errorResponse(status: number, body: string, headers: Record { +describe("HttpClient", () => { let mockFetch: ReturnType; beforeEach(() => { mockFetch = vi.fn().mockResolvedValue(jsonResponse({ ok: true })); - vi.stubGlobal('fetch', mockFetch); + vi.stubGlobal("fetch", mockFetch); }); afterEach(() => { @@ -64,422 +64,422 @@ describe('HttpClient', () => { // ── URL building ──────────────────────────────────────────────────────── - describe('URL building', () => { - it('builds a correct URL for a simple GET', async () => { + describe("URL building", () => { + it("builds a correct URL for a simple GET", async () => { const http = createHttpClient(); - await http.get('/eod/AAPL.US'); + await http.get("/eod/AAPL.US"); const url: string = mockFetch.mock.calls[0][0]; - expect(url).toContain('eodhd.com/api/eod/AAPL.US'); - expect(url).toContain('api_token=demo'); - expect(url).toContain('fmt=json'); + expect(url).toContain("eodhd.com/api/eod/AAPL.US"); + expect(url).toContain("api_token=demo"); + expect(url).toContain("fmt=json"); }); - it('appends query params to the URL', async () => { + it("appends query params to the URL", async () => { const http = createHttpClient(); - await http.get('/eod/AAPL.US', { from: '2025-01-01', to: '2025-06-01', period: 'd' }); + await http.get("/eod/AAPL.US", { from: "2025-01-01", to: "2025-06-01", period: "d" }); const url: string = mockFetch.mock.calls[0][0]; - expect(url).toContain('from=2025-01-01'); - expect(url).toContain('to=2025-06-01'); - expect(url).toContain('period=d'); + expect(url).toContain("from=2025-01-01"); + expect(url).toContain("to=2025-06-01"); + expect(url).toContain("period=d"); }); - it('omits null and undefined params', async () => { + it("omits null and undefined params", async () => { const http = createHttpClient(); - await http.get('/eod/AAPL.US', { from: '2025-01-01', to: undefined, period: null }); + await http.get("/eod/AAPL.US", { from: "2025-01-01", to: undefined, period: null }); const url: string = mockFetch.mock.calls[0][0]; - expect(url).toContain('from=2025-01-01'); - expect(url).not.toContain('to='); - expect(url).not.toContain('period='); + expect(url).toContain("from=2025-01-01"); + expect(url).not.toContain("to="); + expect(url).not.toContain("period="); }); - it('strips leading slash from path', async () => { + it("strips leading slash from path", async () => { const http = createHttpClient(); - await http.get('/eod/AAPL.US'); + await http.get("/eod/AAPL.US"); const url: string = mockFetch.mock.calls[0][0]; // Should not result in double slashes like api//eod expect(url).not.toMatch(/api\/\/eod/); }); - it('handles base URL without trailing slash', async () => { - const http = createHttpClient({ baseUrl: 'https://eodhd.com/api' }); - await http.get('/eod/AAPL.US'); + it("handles base URL without trailing slash", async () => { + const http = createHttpClient({ baseUrl: "https://eodhd.com/api" }); + await http.get("/eod/AAPL.US"); const url: string = mockFetch.mock.calls[0][0]; - expect(url).toContain('eodhd.com/api/eod/AAPL.US'); + expect(url).toContain("eodhd.com/api/eod/AAPL.US"); }); - it('converts numeric param values to strings', async () => { + it("converts numeric param values to strings", async () => { const http = createHttpClient(); - await http.get('/ticks', { limit: 100 }); + await http.get("/ticks", { limit: 100 }); const url: string = mockFetch.mock.calls[0][0]; - expect(url).toContain('limit=100'); + expect(url).toContain("limit=100"); }); }); // ── GET method ────────────────────────────────────────────────────────── - describe('get()', () => { - it('uses GET method', async () => { + describe("get()", () => { + it("uses GET method", async () => { const http = createHttpClient(); - await http.get('/user'); + await http.get("/user"); const opts = mockFetch.mock.calls[0][1]; - expect(opts.method).toBe('GET'); + expect(opts.method).toBe("GET"); }); - it('returns parsed JSON body', async () => { - mockFetch.mockResolvedValue(jsonResponse({ name: 'Test User' })); + it("returns parsed JSON body", async () => { + mockFetch.mockResolvedValue(jsonResponse({ name: "Test User" })); const http = createHttpClient(); - const result = await http.get('/user'); + const result = await http.get("/user"); - expect(result).toEqual({ name: 'Test User' }); + expect(result).toEqual({ name: "Test User" }); }); - it('includes AbortSignal with timeout', async () => { + it("includes AbortSignal with timeout", async () => { const http = createHttpClient({ timeout: 3000 }); - await http.get('/user'); + await http.get("/user"); const opts = mockFetch.mock.calls[0][1]; expect(opts.signal).toBeDefined(); }); - it('always adds fmt=json', async () => { + it("always adds fmt=json", async () => { const http = createHttpClient(); - await http.get('/eod/AAPL.US'); + await http.get("/eod/AAPL.US"); const url: string = mockFetch.mock.calls[0][0]; - expect(url).toContain('fmt=json'); + expect(url).toContain("fmt=json"); }); }); // ── getBuffer method ──────────────────────────────────────────────────── - describe('getBuffer()', () => { - it('uses GET method and returns ArrayBuffer', async () => { + describe("getBuffer()", () => { + it("uses GET method and returns ArrayBuffer", async () => { const buf = new ArrayBuffer(16); mockFetch.mockResolvedValue({ ok: true, status: 200, arrayBuffer: () => Promise.resolve(buf), - text: () => Promise.resolve(''), + text: () => Promise.resolve(""), headers: new Headers(), }); const http = createHttpClient(); - const result = await http.getBuffer('/logo/AAPL.US'); + const result = await http.getBuffer("/logo/AAPL.US"); expect(result).toBe(buf); const opts = mockFetch.mock.calls[0][1]; - expect(opts.method).toBe('GET'); + expect(opts.method).toBe("GET"); }); - it('does not add fmt=json for buffer requests', async () => { + it("does not add fmt=json for buffer requests", async () => { mockFetch.mockResolvedValue({ ok: true, status: 200, arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), - text: () => Promise.resolve(''), + text: () => Promise.resolve(""), headers: new Headers(), }); const http = createHttpClient(); - await http.getBuffer('/logo/AAPL.US'); + await http.getBuffer("/logo/AAPL.US"); const url: string = mockFetch.mock.calls[0][0]; - expect(url).not.toContain('fmt=json'); + expect(url).not.toContain("fmt=json"); }); - it('still includes api_token', async () => { + it("still includes api_token", async () => { mockFetch.mockResolvedValue({ ok: true, status: 200, arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), - text: () => Promise.resolve(''), + text: () => Promise.resolve(""), headers: new Headers(), }); - const http = createHttpClient({ apiToken: 'mykey' }); - await http.getBuffer('/logo/AAPL.US'); + const http = createHttpClient({ apiToken: "mykey" }); + await http.getBuffer("/logo/AAPL.US"); const url: string = mockFetch.mock.calls[0][0]; - expect(url).toContain('api_token=mykey'); + expect(url).toContain("api_token=mykey"); }); }); // ── POST method ───────────────────────────────────────────────────────── - describe('post()', () => { - it('uses POST method with JSON body', async () => { + describe("post()", () => { + it("uses POST method with JSON body", async () => { const http = createHttpClient(); - await http.post('/mp/praams/explore/equity', {}, { regions: ['US'] }); + await http.post("/mp/praams/explore/equity", {}, { regions: ["US"] }); const opts = mockFetch.mock.calls[0][1]; - expect(opts.method).toBe('POST'); - expect(opts.headers['Content-Type']).toBe('application/json'); - expect(JSON.parse(opts.body)).toEqual({ regions: ['US'] }); + expect(opts.method).toBe("POST"); + expect(opts.headers["Content-Type"]).toBe("application/json"); + expect(JSON.parse(opts.body)).toEqual({ regions: ["US"] }); }); - it('adds api_token and fmt=json to URL', async () => { + it("adds api_token and fmt=json to URL", async () => { const http = createHttpClient(); - await http.post('/mp/praams/explore/equity', { skip: 0 }, {}); + await http.post("/mp/praams/explore/equity", { skip: 0 }, {}); const url: string = mockFetch.mock.calls[0][0]; - expect(url).toContain('api_token=demo'); - expect(url).toContain('fmt=json'); - expect(url).toContain('skip=0'); + expect(url).toContain("api_token=demo"); + expect(url).toContain("fmt=json"); + expect(url).toContain("skip=0"); }); - it('returns parsed JSON response', async () => { - mockFetch.mockResolvedValue(jsonResponse([{ ticker: 'AAPL.US' }])); + it("returns parsed JSON response", async () => { + mockFetch.mockResolvedValue(jsonResponse([{ ticker: "AAPL.US" }])); const http = createHttpClient(); - const result = await http.post('/mp/praams/explore/equity'); + const result = await http.post("/mp/praams/explore/equity"); - expect(result).toEqual([{ ticker: 'AAPL.US' }]); + expect(result).toEqual([{ ticker: "AAPL.US" }]); }); }); // ── Error handling ────────────────────────────────────────────────────── - describe('error handling', () => { - it('throws EODHDError on non-ok response', async () => { - mockFetch.mockResolvedValue(errorResponse(401, 'Unauthorized')); + describe("error handling", () => { + it("throws EODHDError on non-ok response", async () => { + mockFetch.mockResolvedValue(errorResponse(401, "Unauthorized")); const http = createHttpClient(); - await expect(http.get('/user')).rejects.toThrow(EODHDError); + await expect(http.get("/user")).rejects.toThrow(EODHDError); }); - it('includes status code in the error', async () => { - mockFetch.mockResolvedValue(errorResponse(403, 'Forbidden')); + it("includes status code in the error", async () => { + mockFetch.mockResolvedValue(errorResponse(403, "Forbidden")); const http = createHttpClient(); try { - await http.get('/user'); - expect.unreachable('should have thrown'); + await http.get("/user"); + expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(EODHDError); expect((err as EODHDError).statusCode).toBe(403); } }); - it('includes response body in error message', async () => { - mockFetch.mockResolvedValue(errorResponse(429, 'Rate limit exceeded')); + it("includes response body in error message", async () => { + mockFetch.mockResolvedValue(errorResponse(429, "Rate limit exceeded")); const http = createHttpClient(); try { - await http.get('/user'); - expect.unreachable('should have thrown'); + await http.get("/user"); + expect.unreachable("should have thrown"); } catch (err) { - expect((err as EODHDError).message).toContain('Rate limit exceeded'); + expect((err as EODHDError).message).toContain("Rate limit exceeded"); } }); - it('falls back to statusText when text() fails', async () => { + it("falls back to statusText when text() fails", async () => { mockFetch.mockResolvedValue({ ok: false, status: 500, - statusText: 'Internal Server Error', - text: () => Promise.reject(new Error('fail')), + statusText: "Internal Server Error", + text: () => Promise.reject(new Error("fail")), headers: new Headers(), }); const http = createHttpClient(); try { - await http.get('/user'); - expect.unreachable('should have thrown'); + await http.get("/user"); + expect.unreachable("should have thrown"); } catch (err) { - expect((err as EODHDError).message).toContain('Internal Server Error'); + expect((err as EODHDError).message).toContain("Internal Server Error"); expect((err as EODHDError).statusCode).toBe(500); } }); - it('uses statusText when response body is empty', async () => { + it("uses statusText when response body is empty", async () => { mockFetch.mockResolvedValue({ ok: false, status: 502, - statusText: 'Bad Gateway', - text: () => Promise.resolve(''), + statusText: "Bad Gateway", + text: () => Promise.resolve(""), headers: new Headers(), }); const http = createHttpClient(); try { - await http.get('/user'); - expect.unreachable('should have thrown'); + await http.get("/user"); + expect.unreachable("should have thrown"); } catch (err) { - expect((err as EODHDError).message).toContain('Bad Gateway'); + expect((err as EODHDError).message).toContain("Bad Gateway"); } }); - it('throws EODHDError on non-ok response for getBuffer', async () => { - mockFetch.mockResolvedValue(errorResponse(404, 'Not Found')); + it("throws EODHDError on non-ok response for getBuffer", async () => { + mockFetch.mockResolvedValue(errorResponse(404, "Not Found")); const http = createHttpClient(); - await expect(http.getBuffer('/logo/INVALID')).rejects.toThrow(EODHDError); + await expect(http.getBuffer("/logo/INVALID")).rejects.toThrow(EODHDError); }); - it('throws EODHDError on non-ok response for post', async () => { - mockFetch.mockResolvedValue(errorResponse(400, 'Bad Request')); + it("throws EODHDError on non-ok response for post", async () => { + mockFetch.mockResolvedValue(errorResponse(400, "Bad Request")); const http = createHttpClient(); - await expect(http.post('/something', {}, {})).rejects.toThrow(EODHDError); + await expect(http.post("/something", {}, {})).rejects.toThrow(EODHDError); }); }); // ── Specific error types ───────────────────────────────────────────────── - describe('specific error types', () => { - it('throws EODHDAuthError on 401', async () => { - mockFetch.mockResolvedValue(errorResponse(401, 'Unauthorized')); + describe("specific error types", () => { + it("throws EODHDAuthError on 401", async () => { + mockFetch.mockResolvedValue(errorResponse(401, "Unauthorized")); const http = createHttpClient(); - await expect(http.get('/user')).rejects.toThrow(EODHDAuthError); + await expect(http.get("/user")).rejects.toThrow(EODHDAuthError); }); - it('throws EODHDAuthError on 403', async () => { - mockFetch.mockResolvedValue(errorResponse(403, 'Forbidden')); + it("throws EODHDAuthError on 403", async () => { + mockFetch.mockResolvedValue(errorResponse(403, "Forbidden")); const http = createHttpClient(); try { - await http.get('/user'); - expect.unreachable('should have thrown'); + await http.get("/user"); + expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(EODHDAuthError); expect((err as EODHDAuthError).statusCode).toBe(403); - expect((err as EODHDAuthError).code).toBe('auth_error'); + expect((err as EODHDAuthError).code).toBe("auth_error"); } }); - it('throws EODHDRateLimitError on 429', async () => { - mockFetch.mockResolvedValue(errorResponse(429, 'Rate limit exceeded')); + it("throws EODHDRateLimitError on 429", async () => { + mockFetch.mockResolvedValue(errorResponse(429, "Rate limit exceeded")); const http = createHttpClient(); - await expect(http.get('/user')).rejects.toThrow(EODHDRateLimitError); + await expect(http.get("/user")).rejects.toThrow(EODHDRateLimitError); }); - it('parses Retry-After header as seconds', async () => { - mockFetch.mockResolvedValue(errorResponse(429, 'Rate limit exceeded', { 'Retry-After': '30' })); + it("parses Retry-After header as seconds", async () => { + mockFetch.mockResolvedValue(errorResponse(429, "Rate limit exceeded", { "Retry-After": "30" })); const http = createHttpClient(); try { - await http.get('/user'); - expect.unreachable('should have thrown'); + await http.get("/user"); + expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(EODHDRateLimitError); expect((err as EODHDRateLimitError).retryAfter).toBe(30); } }); - it('retryAfter is undefined when Retry-After header missing', async () => { - mockFetch.mockResolvedValue(errorResponse(429, 'Rate limit exceeded')); + it("retryAfter is undefined when Retry-After header missing", async () => { + mockFetch.mockResolvedValue(errorResponse(429, "Rate limit exceeded")); const http = createHttpClient(); try { - await http.get('/user'); - expect.unreachable('should have thrown'); + await http.get("/user"); + expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(EODHDRateLimitError); expect((err as EODHDRateLimitError).retryAfter).toBeUndefined(); } }); - it('throws EODHDError with server_error code for 5xx', async () => { - mockFetch.mockResolvedValue(errorResponse(503, 'Service Unavailable')); + it("throws EODHDError with server_error code for 5xx", async () => { + mockFetch.mockResolvedValue(errorResponse(503, "Service Unavailable")); const http = createHttpClient(); try { - await http.get('/user'); - expect.unreachable('should have thrown'); + await http.get("/user"); + expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(EODHDError); - expect((err as EODHDError).code).toBe('server_error'); + expect((err as EODHDError).code).toBe("server_error"); } }); - it('throws EODHDError with client_error code for 4xx', async () => { - mockFetch.mockResolvedValue(errorResponse(404, 'Not Found')); + it("throws EODHDError with client_error code for 4xx", async () => { + mockFetch.mockResolvedValue(errorResponse(404, "Not Found")); const http = createHttpClient(); try { - await http.get('/user'); - expect.unreachable('should have thrown'); + await http.get("/user"); + expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(EODHDError); - expect((err as EODHDError).code).toBe('client_error'); + expect((err as EODHDError).code).toBe("client_error"); } }); }); // ── Fetch error wrapping ───────────────────────────────────────────────── - describe('fetch error wrapping', () => { - it('wraps AbortError as EODHDTimeoutError', async () => { - const abortError = new DOMException('The operation was aborted', 'AbortError'); + describe("fetch error wrapping", () => { + it("wraps AbortError as EODHDTimeoutError", async () => { + const abortError = new DOMException("The operation was aborted", "AbortError"); mockFetch.mockRejectedValue(abortError); const http = createHttpClient(); - await expect(http.get('/user')).rejects.toThrow(EODHDTimeoutError); + await expect(http.get("/user")).rejects.toThrow(EODHDTimeoutError); }); - it('wraps TimeoutError DOMException as EODHDTimeoutError', async () => { - const timeoutError = new DOMException('The operation timed out', 'TimeoutError'); + it("wraps TimeoutError DOMException as EODHDTimeoutError", async () => { + const timeoutError = new DOMException("The operation timed out", "TimeoutError"); mockFetch.mockRejectedValue(timeoutError); const http = createHttpClient(); - await expect(http.get('/user')).rejects.toThrow(EODHDTimeoutError); + await expect(http.get("/user")).rejects.toThrow(EODHDTimeoutError); }); - it('wraps generic fetch error as EODHDNetworkError', async () => { - mockFetch.mockRejectedValue(new TypeError('Failed to fetch')); + it("wraps generic fetch error as EODHDNetworkError", async () => { + mockFetch.mockRejectedValue(new TypeError("Failed to fetch")); const http = createHttpClient(); - await expect(http.get('/user')).rejects.toThrow(EODHDNetworkError); + await expect(http.get("/user")).rejects.toThrow(EODHDNetworkError); }); - it('wraps fetch error for getBuffer', async () => { - mockFetch.mockRejectedValue(new TypeError('Failed to fetch')); + it("wraps fetch error for getBuffer", async () => { + mockFetch.mockRejectedValue(new TypeError("Failed to fetch")); const http = createHttpClient(); - await expect(http.getBuffer('/logo/AAPL')).rejects.toThrow(EODHDNetworkError); + await expect(http.getBuffer("/logo/AAPL")).rejects.toThrow(EODHDNetworkError); }); - it('wraps fetch error for post', async () => { - mockFetch.mockRejectedValue(new TypeError('Failed to fetch')); + it("wraps fetch error for post", async () => { + mockFetch.mockRejectedValue(new TypeError("Failed to fetch")); const http = createHttpClient(); - await expect(http.post('/something')).rejects.toThrow(EODHDNetworkError); + await expect(http.post("/something")).rejects.toThrow(EODHDNetworkError); }); - it('timeout wrapping preserves message', async () => { - const abortError = new DOMException('The operation was aborted', 'AbortError'); + it("timeout wrapping preserves message", async () => { + const abortError = new DOMException("The operation was aborted", "AbortError"); mockFetch.mockRejectedValue(abortError); const http = createHttpClient(); try { - await http.get('/user'); - expect.unreachable('should have thrown'); + await http.get("/user"); + expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(EODHDTimeoutError); - expect((err as EODHDTimeoutError).message).toContain('timed out'); - expect((err as EODHDTimeoutError).code).toBe('timeout'); + expect((err as EODHDTimeoutError).message).toContain("timed out"); + expect((err as EODHDTimeoutError).code).toBe("timeout"); } }); - it('network error preserves original message', async () => { - mockFetch.mockRejectedValue(new TypeError('net::ERR_CONNECTION_REFUSED')); + it("network error preserves original message", async () => { + mockFetch.mockRejectedValue(new TypeError("net::ERR_CONNECTION_REFUSED")); const http = createHttpClient(); try { - await http.get('/user'); - expect.unreachable('should have thrown'); + await http.get("/user"); + expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(EODHDNetworkError); - expect((err as EODHDNetworkError).message).toContain('ERR_CONNECTION_REFUSED'); - expect((err as EODHDNetworkError).code).toBe('network_error'); + expect((err as EODHDNetworkError).message).toContain("ERR_CONNECTION_REFUSED"); + expect((err as EODHDNetworkError).code).toBe("network_error"); } }); }); @@ -489,25 +489,25 @@ describe('HttpClient', () => { // EODHDError standalone tests // --------------------------------------------------------------------------- -describe('EODHDError', () => { - it('has correct name', () => { - const err = new EODHDError('test', 400); - expect(err.name).toBe('EODHDError'); +describe("EODHDError", () => { + it("has correct name", () => { + const err = new EODHDError("test", 400); + expect(err.name).toBe("EODHDError"); }); - it('has correct statusCode', () => { - const err = new EODHDError('test', 401); + it("has correct statusCode", () => { + const err = new EODHDError("test", 401); expect(err.statusCode).toBe(401); }); - it('includes status code in message', () => { - const err = new EODHDError('Unauthorized', 401); - expect(err.message).toContain('401'); - expect(err.message).toContain('Unauthorized'); + it("includes status code in message", () => { + const err = new EODHDError("Unauthorized", 401); + expect(err.message).toContain("401"); + expect(err.message).toContain("Unauthorized"); }); - it('is an instance of Error', () => { - const err = new EODHDError('test', 500); + it("is an instance of Error", () => { + const err = new EODHDError("test", 500); expect(err).toBeInstanceOf(Error); expect(err).toBeInstanceOf(EODHDError); }); diff --git a/test/logger.test.ts b/test/logger.test.ts index c80d0d2..acf9e63 100644 --- a/test/logger.test.ts +++ b/test/logger.test.ts @@ -1,81 +1,78 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { EODHDClient } from '../src/client.js'; -import { - resolveLogger, - redactUrl, - createConsoleLogger, - NO_OP_LOGGER, -} from '../src/logger.js'; -import type { Logger } from '../src/logger.js'; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { EODHDClient } from "../src/client.js"; +import type { Logger } from "../src/logger.js"; +import { createConsoleLogger, NO_OP_LOGGER, redactUrl, resolveLogger } from "../src/logger.js"; // --------------------------------------------------------------------------- // Unit tests for logger utilities // --------------------------------------------------------------------------- -describe('redactUrl', () => { - it('replaces api_token value with ***', () => { - expect(redactUrl('https://eodhd.com/api/eod/AAPL.US?api_token=secret123&fmt=json')) - .toBe('https://eodhd.com/api/eod/AAPL.US?api_token=***&fmt=json'); +describe("redactUrl", () => { + it("replaces api_token value with ***", () => { + expect(redactUrl("https://eodhd.com/api/eod/AAPL.US?api_token=secret123&fmt=json")).toBe( + "https://eodhd.com/api/eod/AAPL.US?api_token=***&fmt=json", + ); }); - it('returns unchanged URL when no api_token present', () => { - const url = 'https://eodhd.com/api/eod/AAPL.US?fmt=json'; + it("returns unchanged URL when no api_token present", () => { + const url = "https://eodhd.com/api/eod/AAPL.US?fmt=json"; expect(redactUrl(url)).toBe(url); }); - it('handles api_token at end of query string', () => { - expect(redactUrl('https://eodhd.com/api/eod?fmt=json&api_token=mykey')) - .toBe('https://eodhd.com/api/eod?fmt=json&api_token=***'); + it("handles api_token at end of query string", () => { + expect(redactUrl("https://eodhd.com/api/eod?fmt=json&api_token=mykey")).toBe( + "https://eodhd.com/api/eod?fmt=json&api_token=***", + ); }); }); -describe('resolveLogger', () => { +describe("resolveLogger", () => { afterEach(() => { delete process.env.EODHD_LOG; }); - it('returns provided logger when given', () => { + it("returns provided logger when given", () => { const custom: Logger = { debug: vi.fn(), warn: vi.fn(), error: vi.fn() }; expect(resolveLogger(custom)).toBe(custom); }); - it('returns console logger when EODHD_LOG=debug', () => { - process.env.EODHD_LOG = 'debug'; + it("returns console logger when EODHD_LOG=debug", () => { + process.env.EODHD_LOG = "debug"; const logger = resolveLogger(); // Console logger has real functions, not no-ops expect(logger).not.toBe(NO_OP_LOGGER); - expect(typeof logger.debug).toBe('function'); + expect(typeof logger.debug).toBe("function"); }); - it('returns NO_OP_LOGGER when no logger and no env var', () => { + it("returns NO_OP_LOGGER when no logger and no env var", () => { delete process.env.EODHD_LOG; expect(resolveLogger()).toBe(NO_OP_LOGGER); }); }); -describe('createConsoleLogger', () => { - it('prefixes messages with [eodhd]', () => { - const spy = vi.spyOn(console, 'debug').mockImplementation(() => {}); +describe("createConsoleLogger", () => { + it("prefixes messages with [eodhd]", () => { + const spy = vi.spyOn(console, "debug").mockImplementation(() => {}); const logger = createConsoleLogger(); - logger.debug('test message'); - expect(spy).toHaveBeenCalledWith('[eodhd] test message'); + logger.debug("test message"); + expect(spy).toHaveBeenCalledWith("[eodhd] test message"); spy.mockRestore(); }); - it('passes extra args through', () => { - const spy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + it("passes extra args through", () => { + const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); const logger = createConsoleLogger(); - logger.warn('msg', { extra: true }); - expect(spy).toHaveBeenCalledWith('[eodhd] msg', { extra: true }); + logger.warn("msg", { extra: true }); + expect(spy).toHaveBeenCalledWith("[eodhd] msg", { extra: true }); spy.mockRestore(); }); }); -describe('NO_OP_LOGGER', () => { - it('has all methods as no-ops', () => { - expect(() => NO_OP_LOGGER.debug('x')).not.toThrow(); - expect(() => NO_OP_LOGGER.warn('x')).not.toThrow(); - expect(() => NO_OP_LOGGER.error('x')).not.toThrow(); +describe("NO_OP_LOGGER", () => { + it("has all methods as no-ops", () => { + expect(() => NO_OP_LOGGER.debug("x")).not.toThrow(); + expect(() => NO_OP_LOGGER.warn("x")).not.toThrow(); + expect(() => NO_OP_LOGGER.error("x")).not.toThrow(); }); }); @@ -83,17 +80,17 @@ describe('NO_OP_LOGGER', () => { // Integration: logging through EODHDClient // --------------------------------------------------------------------------- -describe('logging integration', () => { +describe("logging integration", () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve([]), - text: () => Promise.resolve('[]'), + text: () => Promise.resolve("[]"), headers: new Headers(), }); beforeEach(() => { - vi.stubGlobal('fetch', mockFetch); + vi.stubGlobal("fetch", mockFetch); mockFetch.mockClear(); }); @@ -102,28 +99,24 @@ describe('logging integration', () => { delete process.env.EODHD_LOG; }); - it('calls logger.debug on request when logger provided', async () => { + it("calls logger.debug on request when logger provided", async () => { const logger: Logger = { debug: vi.fn(), warn: vi.fn(), error: vi.fn() }; - const client = new EODHDClient({ apiToken: 'secret-token', logger }); - await client.eod('AAPL.US'); + const client = new EODHDClient({ apiToken: "secret-token", logger }); + await client.eod("AAPL.US"); expect(logger.debug).toHaveBeenCalled(); // Verify api_token is redacted - const calls = (logger.debug as ReturnType).mock.calls.map( - (c: unknown[]) => c[0], - ); - expect(calls.some((msg: string) => msg.includes('secret-token'))).toBe(false); - expect(calls.some((msg: string) => msg.includes('api_token=***'))).toBe(true); + const calls = (logger.debug as ReturnType).mock.calls.map((c: unknown[]) => c[0]); + expect(calls.some((msg: string) => msg.includes("secret-token"))).toBe(false); + expect(calls.some((msg: string) => msg.includes("api_token=***"))).toBe(true); }); - it('logs request and response with elapsed time', async () => { + it("logs request and response with elapsed time", async () => { const logger: Logger = { debug: vi.fn(), warn: vi.fn(), error: vi.fn() }; - const client = new EODHDClient({ apiToken: 'test', logger }); - await client.eod('AAPL.US'); + const client = new EODHDClient({ apiToken: "test", logger }); + await client.eod("AAPL.US"); - const calls = (logger.debug as ReturnType).mock.calls.map( - (c: unknown[]) => c[0], - ); + const calls = (logger.debug as ReturnType).mock.calls.map((c: unknown[]) => c[0]); // First call: request log with GET expect(calls[0]).toMatch(/GET/); // Second call: response log with status and elapsed ms @@ -131,24 +124,22 @@ describe('logging integration', () => { expect(calls[1]).toMatch(/\d+ms/); }); - it('does not log when no logger and no env var', async () => { + it("does not log when no logger and no env var", async () => { delete process.env.EODHD_LOG; - const spy = vi.spyOn(console, 'debug').mockImplementation(() => {}); - const client = new EODHDClient({ apiToken: 'test' }); - await client.eod('AAPL.US'); + const spy = vi.spyOn(console, "debug").mockImplementation(() => {}); + const client = new EODHDClient({ apiToken: "test" }); + await client.eod("AAPL.US"); expect(spy).not.toHaveBeenCalled(); spy.mockRestore(); }); - it('logs POST requests', async () => { + it("logs POST requests", async () => { const logger: Logger = { debug: vi.fn(), warn: vi.fn(), error: vi.fn() }; - const client = new EODHDClient({ apiToken: 'test', logger }); + const client = new EODHDClient({ apiToken: "test", logger }); // praams.exploreEquity is one of the few POST endpoints - await client.marketplace.praams.exploreEquity({}, { regions: ['US'] }); + await client.marketplace.praams.exploreEquity({}, { regions: ["US"] }); - const calls = (logger.debug as ReturnType).mock.calls.map( - (c: unknown[]) => c[0], - ); + const calls = (logger.debug as ReturnType).mock.calls.map((c: unknown[]) => c[0]); expect(calls[0]).toMatch(/POST/); }); }); diff --git a/test/marketplace.test.ts b/test/marketplace.test.ts index 89404b7..53fe3e2 100644 --- a/test/marketplace.test.ts +++ b/test/marketplace.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { EODHDClient } from '../src/client.js'; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { EODHDClient } from "../src/client.js"; function mockFetchOk(body: unknown = {}) { return vi.fn().mockResolvedValue({ @@ -13,7 +13,7 @@ function mockFetchOk(body: unknown = {}) { } function createClient() { - return new EODHDClient({ apiToken: 'test-token', baseUrl: 'https://eodhd.com/api/', timeout: 5000, maxRetries: 0 }); + return new EODHDClient({ apiToken: "test-token", baseUrl: "https://eodhd.com/api/", timeout: 5000, maxRetries: 0 }); } function getCalledUrl(mock: ReturnType): string { @@ -24,209 +24,208 @@ function getCalledMethod(mock: ReturnType): string { return mock.mock.calls[0][1].method; } -describe('Marketplace: Unicorn Bay', () => { +describe("Marketplace: Unicorn Bay", () => { let fetch: ReturnType; beforeEach(() => { fetch = mockFetchOk(); - vi.stubGlobal('fetch', fetch); + vi.stubGlobal("fetch", fetch); }); afterEach(() => vi.restoreAllMocks()); - it('options.contracts() calls /mp/unicornbay/options/contracts', async () => { + it("options.contracts() calls /mp/unicornbay/options/contracts", async () => { const client = createClient(); - await client.marketplace.unicornbay.options.contracts({ 'filter[underlying_symbol]': 'AAPL' }); - expect(getCalledUrl(fetch)).toContain('/mp/unicornbay/options/contracts'); + await client.marketplace.unicornbay.options.contracts({ "filter[underlying_symbol]": "AAPL" }); + expect(getCalledUrl(fetch)).toContain("/mp/unicornbay/options/contracts"); }); - it('options.eod() calls /mp/unicornbay/options/eod', async () => { + it("options.eod() calls /mp/unicornbay/options/eod", async () => { const client = createClient(); - await client.marketplace.unicornbay.options.eod({ 'filter[underlying_symbol]': 'AAPL' }); - expect(getCalledUrl(fetch)).toContain('/mp/unicornbay/options/eod'); + await client.marketplace.unicornbay.options.eod({ "filter[underlying_symbol]": "AAPL" }); + expect(getCalledUrl(fetch)).toContain("/mp/unicornbay/options/eod"); }); - it('options.underlyingSymbols() calls /mp/unicornbay/options/underlying-symbols', async () => { + it("options.underlyingSymbols() calls /mp/unicornbay/options/underlying-symbols", async () => { const client = createClient(); await client.marketplace.unicornbay.options.underlyingSymbols(); - expect(getCalledUrl(fetch)).toContain('/mp/unicornbay/options/underlying-symbols'); + expect(getCalledUrl(fetch)).toContain("/mp/unicornbay/options/underlying-symbols"); }); - it('spglobal.list() calls /mp/unicornbay/spglobal/list', async () => { + it("spglobal.list() calls /mp/unicornbay/spglobal/list", async () => { const client = createClient(); await client.marketplace.unicornbay.spglobal.list(); - expect(getCalledUrl(fetch)).toContain('/mp/unicornbay/spglobal/list'); + expect(getCalledUrl(fetch)).toContain("/mp/unicornbay/spglobal/list"); }); - it('spglobal.components() calls /mp/unicornbay/spglobal/comp/{symbol}', async () => { + it("spglobal.components() calls /mp/unicornbay/spglobal/comp/{symbol}", async () => { const client = createClient(); - await client.marketplace.unicornbay.spglobal.components('GSPC.INDX'); - expect(getCalledUrl(fetch)).toContain('/mp/unicornbay/spglobal/comp/GSPC.INDX'); + await client.marketplace.unicornbay.spglobal.components("GSPC.INDX"); + expect(getCalledUrl(fetch)).toContain("/mp/unicornbay/spglobal/comp/GSPC.INDX"); }); - it('tickdata() calls /mp/unicornbay/tickdata/ticks', async () => { + it("tickdata() calls /mp/unicornbay/tickdata/ticks", async () => { const client = createClient(); - await client.marketplace.unicornbay.tickdata('AAPL'); + await client.marketplace.unicornbay.tickdata("AAPL"); const url = getCalledUrl(fetch); - expect(url).toContain('/mp/unicornbay/tickdata/ticks'); - expect(url).toContain('s=AAPL'); + expect(url).toContain("/mp/unicornbay/tickdata/ticks"); + expect(url).toContain("s=AAPL"); }); - it('logo() calls /mp/unicornbay/logo/{symbol}', async () => { + it("logo() calls /mp/unicornbay/logo/{symbol}", async () => { const client = createClient(); - await client.marketplace.unicornbay.logo('AAPL.US'); - expect(getCalledUrl(fetch)).toContain('/mp/unicornbay/logo/AAPL.US'); + await client.marketplace.unicornbay.logo("AAPL.US"); + expect(getCalledUrl(fetch)).toContain("/mp/unicornbay/logo/AAPL.US"); }); }); -describe('Marketplace: Trading Hours', () => { +describe("Marketplace: Trading Hours", () => { let fetch: ReturnType; beforeEach(() => { fetch = mockFetchOk(); - vi.stubGlobal('fetch', fetch); + vi.stubGlobal("fetch", fetch); }); afterEach(() => vi.restoreAllMocks()); - it('markets() calls /mp/tradinghours/markets', async () => { + it("markets() calls /mp/tradinghours/markets", async () => { const client = createClient(); await client.marketplace.tradinghours.markets(); - expect(getCalledUrl(fetch)).toContain('/mp/tradinghours/markets'); + expect(getCalledUrl(fetch)).toContain("/mp/tradinghours/markets"); }); - it('details() calls /mp/tradinghours/markets/details', async () => { + it("details() calls /mp/tradinghours/markets/details", async () => { const client = createClient(); - await client.marketplace.tradinghours.details({ 'filter[market]': 'US' }); - expect(getCalledUrl(fetch)).toContain('/mp/tradinghours/markets/details'); + await client.marketplace.tradinghours.details({ "filter[market]": "US" }); + expect(getCalledUrl(fetch)).toContain("/mp/tradinghours/markets/details"); }); - it('lookup() calls /mp/tradinghours/markets/lookup', async () => { + it("lookup() calls /mp/tradinghours/markets/lookup", async () => { const client = createClient(); - await client.marketplace.tradinghours.lookup({ 'filter[symbol]': 'AAPL' }); - expect(getCalledUrl(fetch)).toContain('/mp/tradinghours/markets/lookup'); + await client.marketplace.tradinghours.lookup({ "filter[symbol]": "AAPL" }); + expect(getCalledUrl(fetch)).toContain("/mp/tradinghours/markets/lookup"); }); - it('status() calls /mp/tradinghours/markets/status', async () => { + it("status() calls /mp/tradinghours/markets/status", async () => { const client = createClient(); await client.marketplace.tradinghours.status(); - expect(getCalledUrl(fetch)).toContain('/mp/tradinghours/markets/status'); + expect(getCalledUrl(fetch)).toContain("/mp/tradinghours/markets/status"); }); }); -describe('Marketplace: Praams', () => { +describe("Marketplace: Praams", () => { let fetch: ReturnType; beforeEach(() => { fetch = mockFetchOk(); - vi.stubGlobal('fetch', fetch); + vi.stubGlobal("fetch", fetch); }); afterEach(() => vi.restoreAllMocks()); - it('analyseEquityByTicker() calls correct path', async () => { + it("analyseEquityByTicker() calls correct path", async () => { const client = createClient(); - await client.marketplace.praams.analyseEquityByTicker('AAPL.US'); - expect(getCalledUrl(fetch)).toContain('/mp/praams/analyse/equity/ticker/AAPL.US'); + await client.marketplace.praams.analyseEquityByTicker("AAPL.US"); + expect(getCalledUrl(fetch)).toContain("/mp/praams/analyse/equity/ticker/AAPL.US"); }); - it('analyseEquityByIsin() calls correct path', async () => { + it("analyseEquityByIsin() calls correct path", async () => { const client = createClient(); - await client.marketplace.praams.analyseEquityByIsin('US0378331005'); - expect(getCalledUrl(fetch)).toContain('/mp/praams/analyse/equity/isin/US0378331005'); + await client.marketplace.praams.analyseEquityByIsin("US0378331005"); + expect(getCalledUrl(fetch)).toContain("/mp/praams/analyse/equity/isin/US0378331005"); }); - it('analyseBond() calls correct path', async () => { + it("analyseBond() calls correct path", async () => { const client = createClient(); - await client.marketplace.praams.analyseBond('US912828Z880'); - expect(getCalledUrl(fetch)).toContain('/mp/praams/analyse/bond/US912828Z880'); + await client.marketplace.praams.analyseBond("US912828Z880"); + expect(getCalledUrl(fetch)).toContain("/mp/praams/analyse/bond/US912828Z880"); }); - it('bankIncomeStatementByTicker() calls correct path', async () => { + it("bankIncomeStatementByTicker() calls correct path", async () => { const client = createClient(); - await client.marketplace.praams.bankIncomeStatementByTicker('JPM.US'); - expect(getCalledUrl(fetch)).toContain('/mp/praams/bank/income_statement/ticker/JPM.US'); + await client.marketplace.praams.bankIncomeStatementByTicker("JPM.US"); + expect(getCalledUrl(fetch)).toContain("/mp/praams/bank/income_statement/ticker/JPM.US"); }); - it('bankBalanceSheetByIsin() calls correct path', async () => { + it("bankBalanceSheetByIsin() calls correct path", async () => { const client = createClient(); - await client.marketplace.praams.bankBalanceSheetByIsin('US46625H1005'); - expect(getCalledUrl(fetch)).toContain('/mp/praams/bank/balance_sheet/isin/US46625H1005'); + await client.marketplace.praams.bankBalanceSheetByIsin("US46625H1005"); + expect(getCalledUrl(fetch)).toContain("/mp/praams/bank/balance_sheet/isin/US46625H1005"); }); - it('exploreEquity() uses POST method', async () => { + it("exploreEquity() uses POST method", async () => { const client = createClient(); - await client.marketplace.praams.exploreEquity({ skip: 0, take: 10 }, { regions: ['US'] }); - expect(getCalledMethod(fetch)).toBe('POST'); - expect(getCalledUrl(fetch)).toContain('/mp/praams/explore/equity'); + await client.marketplace.praams.exploreEquity({ skip: 0, take: 10 }, { regions: ["US"] }); + expect(getCalledMethod(fetch)).toBe("POST"); + expect(getCalledUrl(fetch)).toContain("/mp/praams/explore/equity"); }); - it('exploreBond() uses POST method', async () => { + it("exploreBond() uses POST method", async () => { const client = createClient(); await client.marketplace.praams.exploreBond({ skip: 0, take: 10 }, { yieldMin: 3 }); - expect(getCalledMethod(fetch)).toBe('POST'); - expect(getCalledUrl(fetch)).toContain('/mp/praams/explore/bond'); + expect(getCalledMethod(fetch)).toBe("POST"); + expect(getCalledUrl(fetch)).toContain("/mp/praams/explore/bond"); }); - it('reportEquityByTicker() calls correct path', async () => { + it("reportEquityByTicker() calls correct path", async () => { const client = createClient(); - await client.marketplace.praams.reportEquityByTicker('AAPL.US'); - expect(getCalledUrl(fetch)).toContain('/mp/praams/reports/equity/ticker/AAPL.US'); + await client.marketplace.praams.reportEquityByTicker("AAPL.US"); + expect(getCalledUrl(fetch)).toContain("/mp/praams/reports/equity/ticker/AAPL.US"); }); - it('reportBond() calls correct path', async () => { + it("reportBond() calls correct path", async () => { const client = createClient(); - await client.marketplace.praams.reportBond('US912828Z880'); - expect(getCalledUrl(fetch)).toContain('/mp/praams/reports/bond/US912828Z880'); + await client.marketplace.praams.reportBond("US912828Z880"); + expect(getCalledUrl(fetch)).toContain("/mp/praams/reports/bond/US912828Z880"); }); }); -describe('Marketplace: InvestVerte', () => { +describe("Marketplace: InvestVerte", () => { let fetch: ReturnType; beforeEach(() => { fetch = mockFetchOk(); - vi.stubGlobal('fetch', fetch); + vi.stubGlobal("fetch", fetch); }); afterEach(() => vi.restoreAllMocks()); - it('companies() calls /mp/investverte/companies', async () => { + it("companies() calls /mp/investverte/companies", async () => { const client = createClient(); await client.marketplace.investverte.companies(); - expect(getCalledUrl(fetch)).toContain('/mp/investverte/companies'); + expect(getCalledUrl(fetch)).toContain("/mp/investverte/companies"); }); - it('countries() calls /mp/investverte/countries', async () => { + it("countries() calls /mp/investverte/countries", async () => { const client = createClient(); await client.marketplace.investverte.countries(); - expect(getCalledUrl(fetch)).toContain('/mp/investverte/countries'); + expect(getCalledUrl(fetch)).toContain("/mp/investverte/countries"); }); - it('sectors() calls /mp/investverte/sectors', async () => { + it("sectors() calls /mp/investverte/sectors", async () => { const client = createClient(); await client.marketplace.investverte.sectors(); - expect(getCalledUrl(fetch)).toContain('/mp/investverte/sectors'); + expect(getCalledUrl(fetch)).toContain("/mp/investverte/sectors"); }); - it('esg() calls /mp/investverte/esg/{symbol}', async () => { + it("esg() calls /mp/investverte/esg/{symbol}", async () => { const client = createClient(); - await client.marketplace.investverte.esg('AAPL.US', { year: 2024 }); + await client.marketplace.investverte.esg("AAPL.US", { year: 2024 }); const url = getCalledUrl(fetch); - expect(url).toContain('/mp/investverte/esg/AAPL.US'); - expect(url).toContain('year=2024'); + expect(url).toContain("/mp/investverte/esg/AAPL.US"); + expect(url).toContain("year=2024"); }); - it('country() calls /mp/investverte/country/{symbol}', async () => { + it("country() calls /mp/investverte/country/{symbol}", async () => { const client = createClient(); - await client.marketplace.investverte.country('US'); - expect(getCalledUrl(fetch)).toContain('/mp/investverte/country/US'); + await client.marketplace.investverte.country("US"); + expect(getCalledUrl(fetch)).toContain("/mp/investverte/country/US"); }); - it('sector() calls /mp/investverte/sector/{symbol}', async () => { + it("sector() calls /mp/investverte/sector/{symbol}", async () => { const client = createClient(); - await client.marketplace.investverte.sector('Technology'); - expect(getCalledUrl(fetch)).toContain('/mp/investverte/sector/Technology'); + await client.marketplace.investverte.sector("Technology"); + expect(getCalledUrl(fetch)).toContain("/mp/investverte/sector/Technology"); }); }); - diff --git a/test/pagination.test.ts b/test/pagination.test.ts index 5f56cdb..1b5b624 100644 --- a/test/pagination.test.ts +++ b/test/pagination.test.ts @@ -1,9 +1,10 @@ -import { describe, it, expect, vi } from 'vitest'; -import { paginate } from '../src/pagination.js'; +import { describe, expect, it, vi } from "vitest"; +import { paginate } from "../src/pagination.js"; -describe('paginate', () => { - it('iterates pages until empty result', async () => { - const fetcher = vi.fn() +describe("paginate", () => { + it("iterates pages until empty result", async () => { + const fetcher = vi + .fn() .mockResolvedValueOnce([{ id: 1 }, { id: 2 }]) .mockResolvedValueOnce([{ id: 3 }, { id: 4 }]) .mockResolvedValueOnce([]); @@ -15,8 +16,9 @@ describe('paginate', () => { expect(fetcher).toHaveBeenCalledTimes(3); }); - it('stops when page smaller than limit', async () => { - const fetcher = vi.fn() + it("stops when page smaller than limit", async () => { + const fetcher = vi + .fn() .mockResolvedValueOnce([{ id: 1 }, { id: 2 }]) .mockResolvedValueOnce([{ id: 3 }]); const items: unknown[] = []; @@ -27,32 +29,29 @@ describe('paginate', () => { expect(fetcher).toHaveBeenCalledTimes(2); // 2nd page < limit, stops }); - it('toArray() collects all items', async () => { - const fetcher = vi.fn() + it("toArray() collects all items", async () => { + const fetcher = vi + .fn() .mockResolvedValueOnce([{ id: 1 }]) .mockResolvedValueOnce([]); const result = paginate(fetcher, { limit: 10 }); expect(await result.toArray()).toEqual([{ id: 1 }]); }); - it('toArray(max) limits total items', async () => { - const fetcher = vi.fn() - .mockResolvedValueOnce([{ id: 1 }, { id: 2 }, { id: 3 }]); + it("toArray(max) limits total items", async () => { + const fetcher = vi.fn().mockResolvedValueOnce([{ id: 1 }, { id: 2 }, { id: 3 }]); expect(await paginate(fetcher, { limit: 10 }).toArray(2)).toEqual([{ id: 1 }, { id: 2 }]); }); - it('passes correct offset and limit to fetcher', async () => { - const fetcher = vi.fn() - .mockResolvedValueOnce([1, 2]) - .mockResolvedValueOnce([3, 4]) - .mockResolvedValueOnce([]); + it("passes correct offset and limit to fetcher", async () => { + const fetcher = vi.fn().mockResolvedValueOnce([1, 2]).mockResolvedValueOnce([3, 4]).mockResolvedValueOnce([]); await paginate(fetcher, { limit: 2, offset: 10 }).toArray(); expect(fetcher).toHaveBeenNthCalledWith(1, 10, 2); expect(fetcher).toHaveBeenNthCalledWith(2, 12, 2); expect(fetcher).toHaveBeenNthCalledWith(3, 14, 2); }); - it('handles empty first page', async () => { + it("handles empty first page", async () => { const fetcher = vi.fn().mockResolvedValueOnce([]); expect(await paginate(fetcher).toArray()).toEqual([]); expect(fetcher).toHaveBeenCalledTimes(1); diff --git a/test/retry.test.ts b/test/retry.test.ts index 466b9ff..56537dd 100644 --- a/test/retry.test.ts +++ b/test/retry.test.ts @@ -1,141 +1,199 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { EODHDClient } from '../src/client.js'; -import { EODHDError } from '../src/errors.js'; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { EODHDClient } from "../src/client.js"; +import { EODHDError } from "../src/errors.js"; -describe('retry', () => { - beforeEach(() => { vi.useFakeTimers(); }); - afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); }); +describe("retry", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); - it('retries on 500 up to maxRetries', async () => { - const mockFetch = vi.fn() - .mockResolvedValueOnce({ ok: false, status: 500, text: () => Promise.resolve('error'), headers: new Headers() }) - .mockResolvedValueOnce({ ok: false, status: 500, text: () => Promise.resolve('error'), headers: new Headers() }) - .mockResolvedValueOnce({ ok: true, status: 200, json: () => Promise.resolve([]), text: () => Promise.resolve('[]'), headers: new Headers() }); - vi.stubGlobal('fetch', mockFetch); - const client = new EODHDClient({ apiToken: 'test', maxRetries: 2 }); - const promise = client.eod('AAPL.US'); + it("retries on 500 up to maxRetries", async () => { + const mockFetch = vi + .fn() + .mockResolvedValueOnce({ ok: false, status: 500, text: () => Promise.resolve("error"), headers: new Headers() }) + .mockResolvedValueOnce({ ok: false, status: 500, text: () => Promise.resolve("error"), headers: new Headers() }) + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve([]), + text: () => Promise.resolve("[]"), + headers: new Headers(), + }); + vi.stubGlobal("fetch", mockFetch); + const client = new EODHDClient({ apiToken: "test", maxRetries: 2 }); + const promise = client.eod("AAPL.US"); await vi.advanceTimersByTimeAsync(60000); const result = await promise; expect(result).toEqual([]); expect(mockFetch).toHaveBeenCalledTimes(3); }); - it('does not retry on 400 (non-retryable)', async () => { - const mockFetch = vi.fn() - .mockResolvedValue({ ok: false, status: 400, text: () => Promise.resolve('bad request'), headers: new Headers() }); - vi.stubGlobal('fetch', mockFetch); - const client = new EODHDClient({ apiToken: 'test', maxRetries: 2 }); - await expect(client.eod('AAPL.US')).rejects.toThrow(EODHDError); + it("does not retry on 400 (non-retryable)", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: false, + status: 400, + text: () => Promise.resolve("bad request"), + headers: new Headers(), + }); + vi.stubGlobal("fetch", mockFetch); + const client = new EODHDClient({ apiToken: "test", maxRetries: 2 }); + await expect(client.eod("AAPL.US")).rejects.toThrow(EODHDError); expect(mockFetch).toHaveBeenCalledTimes(1); }); - it('respects Retry-After header on 429', async () => { - const headers429 = new Headers({ 'Retry-After': '2' }); - const mockFetch = vi.fn() - .mockResolvedValueOnce({ ok: false, status: 429, text: () => Promise.resolve('rate limited'), headers: headers429 }) - .mockResolvedValueOnce({ ok: true, status: 200, json: () => Promise.resolve([]), text: () => Promise.resolve('[]'), headers: new Headers() }); - vi.stubGlobal('fetch', mockFetch); - const client = new EODHDClient({ apiToken: 'test', maxRetries: 1 }); - const promise = client.eod('AAPL.US'); + it("respects Retry-After header on 429", async () => { + const headers429 = new Headers({ "Retry-After": "2" }); + const mockFetch = vi + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 429, + text: () => Promise.resolve("rate limited"), + headers: headers429, + }) + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve([]), + text: () => Promise.resolve("[]"), + headers: new Headers(), + }); + vi.stubGlobal("fetch", mockFetch); + const client = new EODHDClient({ apiToken: "test", maxRetries: 1 }); + const promise = client.eod("AAPL.US"); await vi.advanceTimersByTimeAsync(5000); await promise; expect(mockFetch).toHaveBeenCalledTimes(2); }); - it('throws after exhausting retries', async () => { - const mockFetch = vi.fn() - .mockResolvedValue({ ok: false, status: 503, text: () => Promise.resolve('unavailable'), headers: new Headers() }); - vi.stubGlobal('fetch', mockFetch); - const client = new EODHDClient({ apiToken: 'test', maxRetries: 2 }); - const promise = client.eod('AAPL.US').catch((e: unknown) => e); + it("throws after exhausting retries", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: false, + status: 503, + text: () => Promise.resolve("unavailable"), + headers: new Headers(), + }); + vi.stubGlobal("fetch", mockFetch); + const client = new EODHDClient({ apiToken: "test", maxRetries: 2 }); + const promise = client.eod("AAPL.US").catch((e: unknown) => e); await vi.advanceTimersByTimeAsync(60000); const err = await promise; expect(err).toBeInstanceOf(EODHDError); expect(mockFetch).toHaveBeenCalledTimes(3); }); - it('maxRetries: 0 disables retry', async () => { - const mockFetch = vi.fn() - .mockResolvedValue({ ok: false, status: 500, text: () => Promise.resolve('error'), headers: new Headers() }); - vi.stubGlobal('fetch', mockFetch); - const client = new EODHDClient({ apiToken: 'test', maxRetries: 0 }); - await expect(client.eod('AAPL.US')).rejects.toThrow(); + it("maxRetries: 0 disables retry", async () => { + const mockFetch = vi + .fn() + .mockResolvedValue({ ok: false, status: 500, text: () => Promise.resolve("error"), headers: new Headers() }); + vi.stubGlobal("fetch", mockFetch); + const client = new EODHDClient({ apiToken: "test", maxRetries: 0 }); + await expect(client.eod("AAPL.US")).rejects.toThrow(); expect(mockFetch).toHaveBeenCalledTimes(1); }); - it('retries on network errors', async () => { - const mockFetch = vi.fn() - .mockRejectedValueOnce(new TypeError('Failed to fetch')) - .mockResolvedValueOnce({ ok: true, status: 200, json: () => Promise.resolve([]), text: () => Promise.resolve('[]'), headers: new Headers() }); - vi.stubGlobal('fetch', mockFetch); - const client = new EODHDClient({ apiToken: 'test', maxRetries: 1 }); - const promise = client.eod('AAPL.US'); + it("retries on network errors", async () => { + const mockFetch = vi + .fn() + .mockRejectedValueOnce(new TypeError("Failed to fetch")) + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve([]), + text: () => Promise.resolve("[]"), + headers: new Headers(), + }); + vi.stubGlobal("fetch", mockFetch); + const client = new EODHDClient({ apiToken: "test", maxRetries: 1 }); + const promise = client.eod("AAPL.US"); await vi.advanceTimersByTimeAsync(60000); const result = await promise; expect(result).toEqual([]); expect(mockFetch).toHaveBeenCalledTimes(2); }); - it('does not retry on 401 (auth error)', async () => { - const mockFetch = vi.fn() - .mockResolvedValue({ ok: false, status: 401, text: () => Promise.resolve('unauthorized'), headers: new Headers() }); - vi.stubGlobal('fetch', mockFetch); - const client = new EODHDClient({ apiToken: 'test', maxRetries: 2 }); - await expect(client.eod('AAPL.US')).rejects.toThrow(EODHDError); + it("does not retry on 401 (auth error)", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: false, + status: 401, + text: () => Promise.resolve("unauthorized"), + headers: new Headers(), + }); + vi.stubGlobal("fetch", mockFetch); + const client = new EODHDClient({ apiToken: "test", maxRetries: 2 }); + await expect(client.eod("AAPL.US")).rejects.toThrow(EODHDError); expect(mockFetch).toHaveBeenCalledTimes(1); }); }); -describe('rate limit headers', () => { - afterEach(() => { vi.restoreAllMocks(); }); +describe("rate limit headers", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); - it('logs warning when rate limit remaining is low', async () => { + it("logs warning when rate limit remaining is low", async () => { const headers = new Headers({ - 'X-RateLimit-Limit': '1000', - 'X-RateLimit-Remaining': '5', - 'X-RateLimit-Reset': '1700000000', + "X-RateLimit-Limit": "1000", + "X-RateLimit-Remaining": "5", + "X-RateLimit-Reset": "1700000000", }); const mockFetch = vi.fn().mockResolvedValue({ - ok: true, status: 200, json: () => Promise.resolve([]), text: () => Promise.resolve('[]'), headers, + ok: true, + status: 200, + json: () => Promise.resolve([]), + text: () => Promise.resolve("[]"), + headers, }); - vi.stubGlobal('fetch', mockFetch); + vi.stubGlobal("fetch", mockFetch); const logger = { debug: vi.fn(), warn: vi.fn(), error: vi.fn() }; - const client = new EODHDClient({ apiToken: 'test', logger }); - await client.eod('AAPL.US'); - expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Rate limit low')); + const client = new EODHDClient({ apiToken: "test", logger }); + await client.eod("AAPL.US"); + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("Rate limit low")); }); - it('does not warn when remaining is above threshold', async () => { + it("does not warn when remaining is above threshold", async () => { const headers = new Headers({ - 'X-RateLimit-Limit': '1000', - 'X-RateLimit-Remaining': '500', - 'X-RateLimit-Reset': '1700000000', + "X-RateLimit-Limit": "1000", + "X-RateLimit-Remaining": "500", + "X-RateLimit-Reset": "1700000000", }); const mockFetch = vi.fn().mockResolvedValue({ - ok: true, status: 200, json: () => Promise.resolve([]), text: () => Promise.resolve('[]'), headers, + ok: true, + status: 200, + json: () => Promise.resolve([]), + text: () => Promise.resolve("[]"), + headers, }); - vi.stubGlobal('fetch', mockFetch); + vi.stubGlobal("fetch", mockFetch); const logger = { debug: vi.fn(), warn: vi.fn(), error: vi.fn() }; - const client = new EODHDClient({ apiToken: 'test', logger }); - await client.eod('AAPL.US'); + const client = new EODHDClient({ apiToken: "test", logger }); + await client.eod("AAPL.US"); expect(logger.warn).not.toHaveBeenCalled(); }); - it('handles missing rate limit headers gracefully', async () => { + it("handles missing rate limit headers gracefully", async () => { const mockFetch = vi.fn().mockResolvedValue({ - ok: true, status: 200, json: () => Promise.resolve([]), text: () => Promise.resolve('[]'), headers: new Headers(), + ok: true, + status: 200, + json: () => Promise.resolve([]), + text: () => Promise.resolve("[]"), + headers: new Headers(), }); - vi.stubGlobal('fetch', mockFetch); + vi.stubGlobal("fetch", mockFetch); const logger = { debug: vi.fn(), warn: vi.fn(), error: vi.fn() }; - const client = new EODHDClient({ apiToken: 'test', logger }); - await client.eod('AAPL.US'); + const client = new EODHDClient({ apiToken: "test", logger }); + await client.eod("AAPL.US"); expect(logger.warn).not.toHaveBeenCalled(); }); }); -describe('calculateDelay', () => { - it('returns a value between floor and capped exponential', async () => { - const { calculateDelay, DEFAULT_RETRY } = await import('../src/retry.js'); +describe("calculateDelay", () => { + it("returns a value between floor and capped exponential", async () => { + const { calculateDelay, DEFAULT_RETRY } = await import("../src/retry.js"); const floor = Math.max(100, DEFAULT_RETRY.initialDelay / 2); for (let i = 0; i < 100; i++) { const delay = calculateDelay(0, DEFAULT_RETRY); @@ -144,8 +202,8 @@ describe('calculateDelay', () => { } }); - it('caps at maxDelay', async () => { - const { calculateDelay } = await import('../src/retry.js'); + it("caps at maxDelay", async () => { + const { calculateDelay } = await import("../src/retry.js"); const opts = { maxRetries: 10, initialDelay: 1000, maxDelay: 5000, multiplier: 2 }; for (let i = 0; i < 100; i++) { const delay = calculateDelay(10, opts); @@ -153,8 +211,8 @@ describe('calculateDelay', () => { } }); - it('increases with attempt number on average', async () => { - const { calculateDelay, DEFAULT_RETRY } = await import('../src/retry.js'); + it("increases with attempt number on average", async () => { + const { calculateDelay, DEFAULT_RETRY } = await import("../src/retry.js"); let sum0 = 0; let sum2 = 0; const N = 1000; diff --git a/test/smoke.ts b/test/smoke.ts index 8cf013f..325429e 100644 --- a/test/smoke.ts +++ b/test/smoke.ts @@ -2,11 +2,11 @@ * Full smoke test for the EODHD Node.js SDK — all endpoints * Usage: EODHD_API_TOKEN=xxx npx tsx test/smoke.ts */ -import { EODHDClient } from '../src/index.js'; +import { EODHDClient } from "../src/index.js"; const apiToken = process.env.EODHD_API_TOKEN; if (!apiToken) { - console.error('Set EODHD_API_TOKEN environment variable'); + console.error("Set EODHD_API_TOKEN environment variable"); process.exit(1); } @@ -31,116 +31,125 @@ async function test(name: string, fn: () => Promise) { } async function main() { - console.log('\n=== EODHD Node.js SDK — Full Smoke Test ===\n'); + console.log("\n=== EODHD Node.js SDK — Full Smoke Test ===\n"); // ── EOD & Price Data ── - console.log('EOD & Price Data:'); - await test('eod', () => client.eod('AAPL.US', { from: '2025-01-01', to: '2025-01-10' })); - await test('realTime', () => client.realTime('AAPL.US')); - await test('intraday', () => client.intraday('AAPL.US', { interval: '1h', from: '1740000000', to: '1740100000' })); - await test('usQuoteDelayed', () => client.usQuoteDelayed({ s: 'AAPL' })); - await test('bulkEod', () => client.bulkEod('US', { symbols: 'AAPL,MSFT', date: '2025-01-02' })); - await test('dividends', () => client.dividends('AAPL.US', { from: '2024-01-01', to: '2024-12-31' })); - await test('splits', () => client.splits('AAPL.US', { from: '2020-01-01', to: '2024-12-31' })); - await test('historicalMarketCap', () => client.historicalMarketCap('AAPL.US', { from: '2025-01-01', to: '2025-01-10' })); - await test('ticks', () => client.ticks({ s: 'AAPL', from: '1740000000', to: '1740001000' })); + console.log("EOD & Price Data:"); + await test("eod", () => client.eod("AAPL.US", { from: "2025-01-01", to: "2025-01-10" })); + await test("realTime", () => client.realTime("AAPL.US")); + await test("intraday", () => client.intraday("AAPL.US", { interval: "1h", from: "1740000000", to: "1740100000" })); + await test("usQuoteDelayed", () => client.usQuoteDelayed({ s: "AAPL" })); + await test("bulkEod", () => client.bulkEod("US", { symbols: "AAPL,MSFT", date: "2025-01-02" })); + await test("dividends", () => client.dividends("AAPL.US", { from: "2024-01-01", to: "2024-12-31" })); + await test("splits", () => client.splits("AAPL.US", { from: "2020-01-01", to: "2024-12-31" })); + await test("historicalMarketCap", () => + client.historicalMarketCap("AAPL.US", { from: "2025-01-01", to: "2025-01-10" })); + await test("ticks", () => client.ticks({ s: "AAPL", from: "1740000000", to: "1740001000" })); // ── Fundamentals ── - console.log('\nFundamentals:'); - await test('fundamentals', () => client.fundamentals('AAPL.US', { filter: 'General' })); - await test('bulkFundamentals', () => client.bulkFundamentals('US', { symbols: 'AAPL', type: 'General' })); - await test('logo (PNG)', () => client.logo('AAPL.US')); - await test('logoSvg', () => client.logoSvg('AAPL.US')); + console.log("\nFundamentals:"); + await test("fundamentals", () => client.fundamentals("AAPL.US", { filter: "General" })); + await test("bulkFundamentals", () => client.bulkFundamentals("US", { symbols: "AAPL", type: "General" })); + await test("logo (PNG)", () => client.logo("AAPL.US")); + await test("logoSvg", () => client.logoSvg("AAPL.US")); // ── Calendar ── - console.log('\nCalendar:'); - await test('calendar.earnings', () => client.calendar.earnings({ from: '2025-03-01', to: '2025-03-07' })); - await test('calendar.trends', () => client.calendar.trends({ symbols: 'AAPL.US' })); - await test('calendar.ipos', () => client.calendar.ipos({ from: '2025-03-01', to: '2025-03-07' })); - await test('calendar.splits', () => client.calendar.splits({ from: '2025-03-01', to: '2025-03-31' })); - await test('calendar.dividends', () => client.calendar.dividends({ 'filter[date_eq]': '2025-03-03' })); + console.log("\nCalendar:"); + await test("calendar.earnings", () => client.calendar.earnings({ from: "2025-03-01", to: "2025-03-07" })); + await test("calendar.trends", () => client.calendar.trends({ symbols: "AAPL.US" })); + await test("calendar.ipos", () => client.calendar.ipos({ from: "2025-03-01", to: "2025-03-07" })); + await test("calendar.splits", () => client.calendar.splits({ from: "2025-03-01", to: "2025-03-31" })); + await test("calendar.dividends", () => client.calendar.dividends({ "filter[date_eq]": "2025-03-03" })); // ── News & Sentiment ── - console.log('\nNews & Sentiment:'); - await test('news', () => client.news({ s: 'AAPL.US', limit: 2 })); - await test('sentiments', () => client.sentiments({ s: 'AAPL.US', from: '2025-01-01', to: '2025-01-10' })); - await test('newsWordWeights', () => client.newsWordWeights({ s: 'AAPL.US', 'page[limit]': 10 })); + console.log("\nNews & Sentiment:"); + await test("news", () => client.news({ s: "AAPL.US", limit: 2 })); + await test("sentiments", () => client.sentiments({ s: "AAPL.US", from: "2025-01-01", to: "2025-01-10" })); + await test("newsWordWeights", () => client.newsWordWeights({ s: "AAPL.US", "page[limit]": 10 })); // ── Screening & Search ── - console.log('\nScreening & Search:'); - await test('screener', () => client.screener({ limit: 3, sort: 'market_capitalization.desc' })); - await test('search', () => client.search('Apple', { limit: 3 })); - await test('idMapping', () => client.idMapping({ 'filter[isin]': 'US0378331005' })); - await test('technical (SMA)', () => client.technical('AAPL.US', { function: 'sma', period: 50, from: '2025-01-01', to: '2025-01-31' })); + console.log("\nScreening & Search:"); + await test("screener", () => client.screener({ limit: 3, sort: "market_capitalization.desc" })); + await test("search", () => client.search("Apple", { limit: 3 })); + await test("idMapping", () => client.idMapping({ "filter[isin]": "US0378331005" })); + await test("technical (SMA)", () => + client.technical("AAPL.US", { function: "sma", period: 50, from: "2025-01-01", to: "2025-01-31" })); // ── Exchanges ── - console.log('\nExchanges:'); - await test('exchanges.list', () => client.exchanges.list()); - await test('exchanges.symbols', () => client.exchanges.symbols('US', { type: 'common_stock' })); - await test('exchanges.details', () => client.exchanges.details('US')); - await test('exchanges.symbolChangeHistory', () => client.exchanges.symbolChangeHistory({ from: '2025-01-01', to: '2025-01-31' })); + console.log("\nExchanges:"); + await test("exchanges.list", () => client.exchanges.list()); + await test("exchanges.symbols", () => client.exchanges.symbols("US", { type: "common_stock" })); + await test("exchanges.details", () => client.exchanges.details("US")); + await test("exchanges.symbolChangeHistory", () => + client.exchanges.symbolChangeHistory({ from: "2025-01-01", to: "2025-01-31" })); // ── Corporate ── - console.log('\nCorporate:'); - await test('insiderTransactions', () => client.insiderTransactions({ code: 'AAPL', limit: 3 })); + console.log("\nCorporate:"); + await test("insiderTransactions", () => client.insiderTransactions({ code: "AAPL", limit: 3 })); // ── Macro & Economic ── - console.log('\nMacro & Economic:'); - await test('macroIndicator', () => client.macroIndicator('USA', { indicator: 'gdp_current_usd' })); - await test('economicEvents', () => client.economicEvents({ from: '2025-03-01', to: '2025-03-07' })); + console.log("\nMacro & Economic:"); + await test("macroIndicator", () => client.macroIndicator("USA", { indicator: "gdp_current_usd" })); + await test("economicEvents", () => client.economicEvents({ from: "2025-03-01", to: "2025-03-07" })); // ── Treasury ── - console.log('\nTreasury:'); - await test('treasury.billRates', () => client.treasury.billRates({ from: '2025-01-01', to: '2025-01-10' })); - await test('treasury.yieldRates', () => client.treasury.yieldRates({ from: '2025-01-01', to: '2025-01-10' })); - await test('treasury.longTermRates', () => client.treasury.longTermRates({ from: '2025-01-01', to: '2025-01-10' })); - await test('treasury.realYieldRates', () => client.treasury.realYieldRates({ from: '2025-01-01', to: '2025-01-10' })); + console.log("\nTreasury:"); + await test("treasury.billRates", () => client.treasury.billRates({ from: "2025-01-01", to: "2025-01-10" })); + await test("treasury.yieldRates", () => client.treasury.yieldRates({ from: "2025-01-01", to: "2025-01-10" })); + await test("treasury.longTermRates", () => client.treasury.longTermRates({ from: "2025-01-01", to: "2025-01-10" })); + await test("treasury.realYieldRates", () => client.treasury.realYieldRates({ from: "2025-01-01", to: "2025-01-10" })); // ── CBOE ── - console.log('\nCBOE:'); - await test('cboe.indices', () => client.cboe.indices()); - await test('cboe.index', () => client.cboe.index({ 'filter[index_code]': 'BUK100P', 'filter[feed_type]': 'eod', 'filter[date]': '2025-01-15' })); + console.log("\nCBOE:"); + await test("cboe.indices", () => client.cboe.indices()); + await test("cboe.index", () => + client.cboe.index({ "filter[index_code]": "BUK100P", "filter[feed_type]": "eod", "filter[date]": "2025-01-15" })); // ── User ── - console.log('\nUser:'); - await test('user', () => client.user()); + console.log("\nUser:"); + await test("user", () => client.user()); // ── Marketplace: Unicorn Bay ── - console.log('\nMarketplace — Unicorn Bay:'); - await test('unicornbay.options.contracts', () => client.marketplace.unicornbay.options.contracts({ 'filter[underlying_symbol]': 'AAPL', 'page[limit]': 3 })); - await test('unicornbay.options.eod', () => client.marketplace.unicornbay.options.eod({ 'filter[underlying_symbol]': 'AAPL', 'page[limit]': 3 })); - await test('unicornbay.options.underlyingSymbols', () => client.marketplace.unicornbay.options.underlyingSymbols({ 'page[limit]': 3 })); - await test('unicornbay.spglobal.list', () => client.marketplace.unicornbay.spglobal.list()); - await test('unicornbay.spglobal.components', () => client.marketplace.unicornbay.spglobal.components('GSPC')); - await test('unicornbay.tickdata', () => client.marketplace.unicornbay.tickdata('AAPL', { from: '1740000000', to: '1740001000' })); - await test('unicornbay.logo', () => client.marketplace.unicornbay.logo('AAPL')); + console.log("\nMarketplace — Unicorn Bay:"); + await test("unicornbay.options.contracts", () => + client.marketplace.unicornbay.options.contracts({ "filter[underlying_symbol]": "AAPL", "page[limit]": 3 })); + await test("unicornbay.options.eod", () => + client.marketplace.unicornbay.options.eod({ "filter[underlying_symbol]": "AAPL", "page[limit]": 3 })); + await test("unicornbay.options.underlyingSymbols", () => + client.marketplace.unicornbay.options.underlyingSymbols({ "page[limit]": 3 })); + await test("unicornbay.spglobal.list", () => client.marketplace.unicornbay.spglobal.list()); + await test("unicornbay.spglobal.components", () => client.marketplace.unicornbay.spglobal.components("GSPC")); + await test("unicornbay.tickdata", () => + client.marketplace.unicornbay.tickdata("AAPL", { from: "1740000000", to: "1740001000" })); + await test("unicornbay.logo", () => client.marketplace.unicornbay.logo("AAPL")); // ── Marketplace: Trading Hours ── - console.log('\nMarketplace — Trading Hours:'); - await test('tradinghours.markets', () => client.marketplace.tradinghours.markets()); - await test('tradinghours.details', () => client.marketplace.tradinghours.details({ market: 'US.NASDAQ' })); - await test('tradinghours.lookup', () => client.marketplace.tradinghours.lookup({ 'filter[name]': 'NASDAQ' })); - await test('tradinghours.status', () => client.marketplace.tradinghours.status({ market: 'US.NASDAQ' })); + console.log("\nMarketplace — Trading Hours:"); + await test("tradinghours.markets", () => client.marketplace.tradinghours.markets()); + await test("tradinghours.details", () => client.marketplace.tradinghours.details({ market: "US.NASDAQ" })); + await test("tradinghours.lookup", () => client.marketplace.tradinghours.lookup({ "filter[name]": "NASDAQ" })); + await test("tradinghours.status", () => client.marketplace.tradinghours.status({ market: "US.NASDAQ" })); // ── Marketplace: PRAAMS ── - console.log('\nMarketplace — PRAAMS:'); - await test('praams.analyseEquityByTicker', () => client.marketplace.praams.analyseEquityByTicker('AAPL.DE')); - await test('praams.analyseEquityByIsin', () => client.marketplace.praams.analyseEquityByIsin('US0378331005')); - await test('praams.reportEquityByTicker', () => client.marketplace.praams.reportEquityByTicker('AAPL.US', { email: 'test@example.com' })); - await test('praams.exploreEquity', () => client.marketplace.praams.exploreEquity({}, {})); + console.log("\nMarketplace — PRAAMS:"); + await test("praams.analyseEquityByTicker", () => client.marketplace.praams.analyseEquityByTicker("AAPL.DE")); + await test("praams.analyseEquityByIsin", () => client.marketplace.praams.analyseEquityByIsin("US0378331005")); + await test("praams.reportEquityByTicker", () => + client.marketplace.praams.reportEquityByTicker("AAPL.US", { email: "test@example.com" })); + await test("praams.exploreEquity", () => client.marketplace.praams.exploreEquity({}, {})); // ── Marketplace: InvestVerte (ESG) ── - console.log('\nMarketplace — InvestVerte (ESG):'); - await test('investverte.companies', () => client.marketplace.investverte.companies()); - await test('investverte.countries', () => client.marketplace.investverte.countries()); - await test('investverte.sectors', () => client.marketplace.investverte.sectors()); - await test('investverte.esg', () => client.marketplace.investverte.esg('AAPL')); + console.log("\nMarketplace — InvestVerte (ESG):"); + await test("investverte.companies", () => client.marketplace.investverte.companies()); + await test("investverte.countries", () => client.marketplace.investverte.countries()); + await test("investverte.sectors", () => client.marketplace.investverte.sectors()); + await test("investverte.esg", () => client.marketplace.investverte.esg("AAPL")); // ── Summary ── - console.log(`\n${'='.repeat(50)}`); + console.log(`\n${"=".repeat(50)}`); console.log(`\x1b[32m${passed} passed\x1b[0m, \x1b[31m${failed} failed\x1b[0m out of ${passed + failed} endpoints`); if (failures.length > 0) { - console.log(`\nFailed: ${failures.join(', ')}`); + console.log(`\nFailed: ${failures.join(", ")}`); } console.log(); diff --git a/test/websocket.test.ts b/test/websocket.test.ts index 3ecf0ac..f4fcca0 100644 --- a/test/websocket.test.ts +++ b/test/websocket.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { EODHDWebSocket } from '../src/websocket.js'; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { EODHDWebSocket } from "../src/websocket.js"; // --------------------------------------------------------------------------- // Mock WebSocket — tracks all instances created @@ -61,12 +61,12 @@ function latestWs(): MockWebSocket { // Test suite // --------------------------------------------------------------------------- -describe('EODHDWebSocket', () => { +describe("EODHDWebSocket", () => { beforeEach(() => { vi.useFakeTimers(); instances.length = 0; // Stub globalThis.WebSocket with our mock class - vi.stubGlobal('WebSocket', MockWebSocket); + vi.stubGlobal("WebSocket", MockWebSocket); }); afterEach(() => { @@ -75,36 +75,36 @@ describe('EODHDWebSocket', () => { }); function createWs( - symbols: string[] = ['AAPL', 'MSFT'], + symbols: string[] = ["AAPL", "MSFT"], options: { maxReconnectAttempts?: number; reconnectInterval?: number } = {}, ) { - return new EODHDWebSocket('test-token', 'us', symbols, options); + return new EODHDWebSocket("test-token", "us", symbols, options); } // ─── 1. Subscribes on connect ─────────────────────────────────────────── - it('sends subscribe message on connect', () => { - const ws = createWs(['AAPL', 'MSFT']); + it("sends subscribe message on connect", () => { + const ws = createWs(["AAPL", "MSFT"]); ws.connect(); const mock = latestWs(); mock.simulateOpen(); expect(mock.sent).toHaveLength(1); const msg = JSON.parse(mock.sent[0]); - expect(msg).toEqual({ action: 'subscribe', symbols: 'AAPL,MSFT' }); + expect(msg).toEqual({ action: "subscribe", symbols: "AAPL,MSFT" }); }); // ─── 2. Emits data events on message ──────────────────────────────────── - it('emits data events on message', () => { + it("emits data events on message", () => { const ws = createWs(); const handler = vi.fn(); - ws.on('data', handler); + ws.on("data", handler); ws.connect(); const mock = latestWs(); mock.simulateOpen(); - const tick = { s: 'AAPL', p: 150.5, v: 100, t: 1234567890 }; + const tick = { s: "AAPL", p: 150.5, v: 100, t: 1234567890 }; mock.simulateMessage(tick); expect(handler).toHaveBeenCalledWith(tick); @@ -112,59 +112,59 @@ describe('EODHDWebSocket', () => { // ─── 3. Emits error on JSON parse failure ─────────────────────────────── - it('emits error event on JSON parse failure', () => { + it("emits error event on JSON parse failure", () => { const ws = createWs(); const errorHandler = vi.fn(); - ws.on('error', errorHandler); + ws.on("error", errorHandler); ws.connect(); const mock = latestWs(); mock.simulateOpen(); - mock.simulateRawMessage('not-valid-json{{{'); + mock.simulateRawMessage("not-valid-json{{{"); expect(errorHandler).toHaveBeenCalledTimes(1); expect(errorHandler.mock.calls[0][0]).toBeInstanceOf(Error); - expect(errorHandler.mock.calls[0][0].message).toContain('parse'); + expect(errorHandler.mock.calls[0][0].message).toContain("parse"); }); // ─── 4. subscribe() sends subscribe message post-connect ──────────────── - it('subscribe() sends subscribe message on open connection', () => { - const ws = createWs(['AAPL']); + it("subscribe() sends subscribe message on open connection", () => { + const ws = createWs(["AAPL"]); ws.connect(); const mock = latestWs(); mock.simulateOpen(); mock.sent = []; // clear initial subscribe - ws.subscribe(['GOOG', 'TSLA']); + ws.subscribe(["GOOG", "TSLA"]); expect(mock.sent).toHaveLength(1); const msg = JSON.parse(mock.sent[0]); - expect(msg).toEqual({ action: 'subscribe', symbols: 'GOOG,TSLA' }); + expect(msg).toEqual({ action: "subscribe", symbols: "GOOG,TSLA" }); }); // ─── 5. unsubscribe() sends unsubscribe message ──────────────────────── - it('unsubscribe() sends unsubscribe message', () => { - const ws = createWs(['AAPL', 'MSFT']); + it("unsubscribe() sends unsubscribe message", () => { + const ws = createWs(["AAPL", "MSFT"]); ws.connect(); const mock = latestWs(); mock.simulateOpen(); mock.sent = []; - ws.unsubscribe(['AAPL']); + ws.unsubscribe(["AAPL"]); expect(mock.sent).toHaveLength(1); const msg = JSON.parse(mock.sent[0]); - expect(msg).toEqual({ action: 'unsubscribe', symbols: 'AAPL' }); + expect(msg).toEqual({ action: "unsubscribe", symbols: "AAPL" }); }); // ─── 6. Does not fire close listeners during reconnect ────────────────── - it('does not fire close listeners during reconnect', () => { - const ws = createWs(['AAPL'], { maxReconnectAttempts: 3, reconnectInterval: 1000 }); + it("does not fire close listeners during reconnect", () => { + const ws = createWs(["AAPL"], { maxReconnectAttempts: 3, reconnectInterval: 1000 }); const closeHandler = vi.fn(); - ws.on('close', closeHandler); + ws.on("close", closeHandler); ws.connect(); latestWs().simulateOpen(); @@ -174,10 +174,10 @@ describe('EODHDWebSocket', () => { expect(closeHandler).not.toHaveBeenCalled(); }); - it('fires close listeners when user calls close()', () => { - const ws = createWs(['AAPL'], { maxReconnectAttempts: 3 }); + it("fires close listeners when user calls close()", () => { + const ws = createWs(["AAPL"], { maxReconnectAttempts: 3 }); const closeHandler = vi.fn(); - ws.on('close', closeHandler); + ws.on("close", closeHandler); ws.connect(); latestWs().simulateOpen(); @@ -186,12 +186,12 @@ describe('EODHDWebSocket', () => { expect(closeHandler).toHaveBeenCalledTimes(1); }); - it('fires close listeners after reconnect exhausted', () => { - const ws = createWs(['AAPL'], { maxReconnectAttempts: 2, reconnectInterval: 100 }); + it("fires close listeners after reconnect exhausted", () => { + const ws = createWs(["AAPL"], { maxReconnectAttempts: 2, reconnectInterval: 100 }); const closeHandler = vi.fn(); const reconnectFailedHandler = vi.fn(); - ws.on('close', closeHandler); - ws.on('reconnectFailed', reconnectFailedHandler); + ws.on("close", closeHandler); + ws.on("reconnectFailed", reconnectFailedHandler); ws.connect(); latestWs().simulateOpen(); @@ -214,10 +214,10 @@ describe('EODHDWebSocket', () => { // ─── 7. Emits reconnectFailed after max attempts exhausted ────────────── - it('emits reconnectFailed after max attempts exhausted', () => { - const ws = createWs(['AAPL'], { maxReconnectAttempts: 1, reconnectInterval: 100 }); + it("emits reconnectFailed after max attempts exhausted", () => { + const ws = createWs(["AAPL"], { maxReconnectAttempts: 1, reconnectInterval: 100 }); const reconnectHandler = vi.fn(); - ws.on('reconnectFailed', reconnectHandler); + ws.on("reconnectFailed", reconnectHandler); ws.connect(); latestWs().simulateOpen(); @@ -234,29 +234,29 @@ describe('EODHDWebSocket', () => { // ─── 8. Skips status messages ─────────────────────────────────────────── - it('skips messages with status_code property', () => { + it("skips messages with status_code property", () => { const ws = createWs(); const handler = vi.fn(); - ws.on('data', handler); + ws.on("data", handler); ws.connect(); latestWs().simulateOpen(); - latestWs().simulateMessage({ status_code: 200, message: 'connected' }); + latestWs().simulateMessage({ status_code: 200, message: "connected" }); expect(handler).not.toHaveBeenCalled(); }); // ─── Additional: multiple data listeners ──────────────────────────────── - it('supports multiple data listeners', () => { + it("supports multiple data listeners", () => { const ws = createWs(); const h1 = vi.fn(); const h2 = vi.fn(); - ws.on('data', h1).on('data', h2); + ws.on("data", h1).on("data", h2); ws.connect(); latestWs().simulateOpen(); - latestWs().simulateMessage({ s: 'AAPL', p: 100, t: 1 }); + latestWs().simulateMessage({ s: "AAPL", p: 100, t: 1 }); expect(h1).toHaveBeenCalledTimes(1); expect(h2).toHaveBeenCalledTimes(1); @@ -264,7 +264,7 @@ describe('EODHDWebSocket', () => { // ─── Empty symbols array does not send subscribe ──────────────────────── - it('does not send subscribe if symbols array is empty', () => { + it("does not send subscribe if symbols array is empty", () => { const ws = createWs([]); ws.connect(); latestWs().simulateOpen(); @@ -274,10 +274,10 @@ describe('EODHDWebSocket', () => { // ─── Reconnect resets attempt counter on successful connect ───────────── - it('resets reconnect counter on successful reconnect', () => { - const ws = createWs(['AAPL'], { maxReconnectAttempts: 2, reconnectInterval: 100 }); + it("resets reconnect counter on successful reconnect", () => { + const ws = createWs(["AAPL"], { maxReconnectAttempts: 2, reconnectInterval: 100 }); const reconnectHandler = vi.fn(); - ws.on('reconnectFailed', reconnectHandler); + ws.on("reconnectFailed", reconnectHandler); ws.connect(); latestWs().simulateOpen(); diff --git a/vitest.config.ts b/vitest.config.ts index 3353345..17786fa 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,5 +4,16 @@ export default defineConfig({ test: { include: ['test/**/*.test.ts'], globals: false, + coverage: { + provider: 'v8', + reporter: ['text', 'lcov', 'json-summary'], + include: ['src/**/*.ts'], + thresholds: { + lines: 80, + branches: 75, + functions: 80, + statements: 80, + }, + }, }, });