diff --git a/.env.example b/.env.example index c4eb880..e219840 100644 --- a/.env.example +++ b/.env.example @@ -1,19 +1,63 @@ -CAPTCHA_API_KEY=your_api_key_here +# Copy to .env and fill in. .env is gitignored. +# +# Used by two things: +# - the scripts in examples/, which call `import 'dotenv/config'`; +# - the integration tests, via tests/integration/helpers.js. +# +# Target pages and their widget identifiers are deliberately left empty. This +# repository does not carry links to real sites or their keys, so every target +# is yours to fill in. An integration suite whose variables are unset skips +# itself instead of failing -- see tests/README.md. +# +# What identifies the widget depends on the captcha: reCAPTCHA, Turnstile and +# Yandex use a sitekey, Tencent uses an appId, GeeTest v4 a captchaId. The +# variable names below follow whichever one the vendor actually uses. -RECAPTCHA_V2_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI -RECAPTCHA_V2_URL=https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox.php +# Required for everything below. +CAPTCHA_API_KEY= -RECAPTCHA_V3_SITE_KEY=6LfD3wAVAAAAAKQHYQqhNNYh3zSqLU0YPM-iu0Wc -RECAPTCHA_V3_URL=https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php +# --- Targets for the integration tests --- +# Each pair enables one suite in tests/integration/. Fill in only the types you +# want to check; the rest will skip. -TURNSTILE_SITE_KEY=0x4AAAAAAAJmU3oC9CbBXuBT -TURNSTILE_URL=https://peet.ws/turnstile-test/non-interactive.html +# tests/integration/recaptcha_v2.test.js +RECAPTCHA_V2_URL= +RECAPTCHA_V2_SITE_KEY= -GEETEST_V3_GT=f2ae6cadcf7886856696c46d84d109d1 -GEETEST_V3_CHALLENGE=12345678abc90123d45678e90123f45g6 -GEETEST_V3_URL=https://www.geetest.com/en/demo +# tests/integration/recaptcha_v3.test.js +# RECAPTCHA_V3_PAGE_ACTION is optional: set it when the site passes an action +# to grecaptcha.execute(). +RECAPTCHA_V3_URL= +RECAPTCHA_V3_SITE_KEY= +RECAPTCHA_V3_PAGE_ACTION= -GEETEST_V4_CAPTCHA_ID=e392e65f912c780f2c3ebac7702651de -GEETEST_V4_URL=https://www.geetest.com/en/demo +# tests/integration/turnstile.test.js +TURNSTILE_URL= +TURNSTILE_SITE_KEY= -IMAGE_TO_TEXT_BASE64=iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAAACXBIWXMAAAsTAAALEwEAmpwYAAABaklEQVRIic2VPWtUQRjHfzPnnHv3bnb3ZneNuFkJKgkWVjY2NhZWFn4AEWwsLOwUbIVUtgERxCSFgiAGxEIiCOYDbPIVfBe5u3vuOTMHi7gkJP4Hnup5nv8z7/PMDP8ZMVIA3POcc0YIMUXkAVAVUcixpxpWCsgLWKu8AP4VJcSNMXUAHoJsh1DUlwLWao1i/DuI0CuUWwVa0sTGBB/+Q4qjQw5qZYp2B/UUESFPKK5ZAvSapFy5oAQnRw6CLPK95LmmAt0PDr9ZwnAI6t3nACdEtBniFJDPmHSm87mIdw4zVygIliYiTwH2K5IbhqINkDNjIl8adT3u5BIIHhO52B3Oc0FVT+4BEHuNWHfIDRFZX7vI/H9QIlIvB0R+AnvN6OtYRAVwZ80ICXTHaA+mIgtnmuNNzUGQtkgpYhA7z4goLskAJ0CaK2vIGa0FNv+L4hBZa4idDcAzz3aL8GmG5T8vsl/+zN6LFQAAAABJRU5ErkJggg== \ No newline at end of file +# tests/integration/geetest_v4.test.js +# v4 only. v3 needs a fresh per-session `challenge` scraped from the target, +# so it cannot be driven from static configuration. +GEETEST_V4_URL= +GEETEST_V4_CAPTCHA_ID= + +# tests/integration/yandex_smartcaptcha.test.js +YANDEX_SMARTCAPTCHA_URL= +YANDEX_SMARTCAPTCHA_SITE_KEY= + +# tests/integration/tencent.test.js +# TENCENT_CAPTCHA_SCRIPT is optional: set it only when the site loads the +# widget from a non-default script URL. +TENCENT_URL= +TENCENT_APP_ID= +TENCENT_CAPTCHA_SCRIPT= + +# tests/integration/image_to_text.test.js and coordinates.test.js need nothing +# here at all: no page, no widget identifier, just an image, and both images +# ship with the repository (examples/assets/). Those two suites run as-is with +# nothing but the key above. +# +# IMAGE_TO_TEXT_EXPECTED is the one optional knob: set it to the text on +# examples/assets/text-captcha.png to assert the answer itself instead of +# merely asserting that something came back. +IMAGE_TO_TEXT_EXPECTED= diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..747f24f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,82 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + schedule: + # Ночной прогон integration-тестов против реального API + - cron: '0 3 * * *' + workflow_dispatch: + +jobs: + unit: + name: Unit (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Соответствует engines.node: ">=18" в package.json + node-version: [18, 20, 22] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - run: npm ci + + - name: Run unit tests with coverage + run: npm run test:unit -- --coverage + + # Покрытие выгружается один раз, а не с каждой версии Node -- + # цифра от версии не зависит, дубли только зашумят отчёт. + # + # fail-on-error: false -- выгрузка покрытия вспомогательная, и её сбой + # не должен красить джоб, в котором все тесты прошли. Иначе недоступность + # Coveralls или неподключённый репозиторий блокируют мёрж рабочего PR. + # Устаревший бейдж заметен и сам по себе. + - name: Upload coverage to Coveralls + if: matrix.node-version == 20 + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + file: coverage/lcov.info + fail-on-error: false + + integration: + name: Integration (real API) + # Только по расписанию и вручную. На pull_request не запускается намеренно: + # тесты тратят реальный баланс аккаунта, а в PR из форков секрет всё равно + # недоступен -- тесты молча пропустятся и дадут ложно-зелёный результат. + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - name: Fail early if the API key is missing + # Без ключа integration-тесты пропускают сами себя и джоб зеленеет, + # ничего не проверив. Лучше упасть явно. + env: + CAPTCHA_API_KEY: ${{ secrets.CAPTCHA_API_KEY }} + run: | + if [ -z "$CAPTCHA_API_KEY" ]; then + echo "::error::secrets.CAPTCHA_API_KEY is not set -- integration tests would silently skip" + exit 1 + fi + + - name: Run integration tests + env: + CAPTCHA_API_KEY: ${{ secrets.CAPTCHA_API_KEY }} + run: npm run test:integration diff --git a/.gitignore b/.gitignore index 598c4ef..0143c10 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ node_modules/ *.log .DS_Store dist/ -coverage/ \ No newline at end of file +coverage/ +.claude/ +todo.md diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..49b9b96 --- /dev/null +++ b/.npmignore @@ -0,0 +1,37 @@ +# Что попадает в публикуемый пакет, в первую очередь определяет поле +# "files": ["src/"] в package.json -- это белый список, и он имеет приоритет +# над этим файлом. Здесь -- второй рубеж на случай, если "files" уберут или +# расширят: тогда лишнее не утечёт в тарбол молча. +# +# Важно: пока этот файл существует, npm НЕ использует .gitignore при упаковке. +# Поэтому всё, что не должно публиковаться, нужно перечислять именно здесь, +# даже если оно уже есть в .gitignore. +# +# Синтаксис -- как в .gitignore. Префикс "./" не работает: писать "tests", +# а не "./tests". + +# Тесты и их конфигурация +tests/ +jest.config.js +coverage/ + +# CI и локальные настройки инструментов +.github/ +.claude/ + +# Примеры и ассеты репозитория: нужны на GitHub, но не потребителю пакета +examples/ +assets/ + +# Переменные окружения +.env +.env.example + +# Документы для контрибьюторов: место им на GitHub, а не в тарболе +CONTRIBUTING.md +CODE_OF_CONDUCT.md +todo.md + +# npm и так всегда включает package.json, README, LICENSE и файл из "main", +# и всегда исключает .git, node_modules, .npmrc и npm-debug.log -- +# перечислять их здесь не нужно. diff --git a/README.md b/README.md index c7be66a..40d67a3 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Official JavaScript SDK for the Captcha Solver API. Solve reCAPTCHA v2/v3, Cloud ## Installation ```bash -# npm install captcha-sdk +# npm install captcha-sdk from github.com npm install git+https://github.com/captcha-solver-api/javascript-sdk.git ``` @@ -41,22 +41,22 @@ export CAPTCHA_API_KEY=your_api_key ``` ```javascript import { CaptchaClient } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: process.env.CAPTCHA_API_KEY }); +const captchaSolver = new CaptchaClient({ clientKey: process.env.CAPTCHA_API_KEY }); ``` Or just pass the key directly, without an environment variable: ```javascript -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); ``` ## Quick Start Solve a reCAPTCHA v2 in 4 lines. ```javascript import { CaptchaClient, Tasks } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); const task = new Tasks.RecaptchaV2Proxyless({ websiteURL: 'https://example.com/login', websiteKey: '6Le-xxxxxxxxx' }); -const result = await client.solve(task); +const result = await captchaSolver.solve(task); console.log(result.gRecaptchaResponse); ``` Runnable versions of every example below live in [examples/async](examples/async) (async/await @@ -80,7 +80,7 @@ the same `CaptchaClient`, since JavaScript has no blocking HTTP client to mirror ### reCAPTCHA v2 with proxy ```javascript import { CaptchaClient, Tasks } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); const task = new Tasks.RecaptchaV2({ websiteURL: 'https://example.com/login', websiteKey: '6Le-xxxxxxxxx', @@ -90,43 +90,43 @@ const task = new Tasks.RecaptchaV2({ proxyLogin: 'user', proxyPassword: 'password' }); -const result = await client.solve(task); +const result = await captchaSolver.solve(task); console.log(result.gRecaptchaResponse); ``` ### reCAPTCHA v2 Enterprise ```javascript import { CaptchaClient, Tasks } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); const task = new Tasks.RecaptchaV2EnterpriseProxyless({ websiteURL: 'https://example.com/login', websiteKey: '6Le-xxxxxxxxx', enterprisePayload: { s: 'data-s-value' } }); -const result = await client.solve(task); +const result = await captchaSolver.solve(task); console.log(result.gRecaptchaResponse); ``` ### reCAPTCHA v3 ```javascript import { CaptchaClient, Tasks } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); const task = new Tasks.RecaptchaV3Proxyless({ websiteURL: 'https://example.com/login', websiteKey: '6Le-xxxxxxxxx', minScore: 0.7, pageAction: 'login' }); -const result = await client.solve(task); +const result = await captchaSolver.solve(task); console.log(result.gRecaptchaResponse); ``` ### Cloudflare Turnstile ```javascript import { CaptchaClient, Tasks } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); const task = new Tasks.TurnstileProxyless({ websiteURL: 'https://example.com/login', websiteKey: '0x4AAAAAAAxxxxxxxx' }); -const result = await client.solve(task); +const result = await captchaSolver.solve(task); console.log(result.token); ``` ### Image to Text @@ -134,50 +134,50 @@ console.log(result.token); import { CaptchaClient, Tasks } from 'captcha-sdk'; import fs from 'fs'; const imageBase64 = fs.readFileSync('captcha.png').toString('base64'); -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); const task = new Tasks.ImageToText({ body: imageBase64, numeric: 1, minLength: 4, maxLength: 6 }); -const result = await client.solve(task); +const result = await captchaSolver.solve(task); console.log(result.text); ``` ### GeeTest v3 ```javascript import { CaptchaClient, Tasks } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); const task = new Tasks.GeeTestProxyless({ websiteURL: 'https://example.com/login', gt: 'f2ae6cadcf7886856696c46d84d109d1', challenge: '12345678abc90123d45678e90123f45g6' }); -const result = await client.solve(task); +const result = await captchaSolver.solve(task); console.log(result.validate); console.log(result.seccode); ``` ### GeeTest v4 ```javascript import { CaptchaClient, Tasks } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); const task = new Tasks.GeeTestProxyless({ websiteURL: 'https://example.com/login', version: 4, initParameters: { captcha_id: 'e392e65f912c780f2c3ebac7702651de' } }); -const result = await client.solve(task); +const result = await captchaSolver.solve(task); console.log(result.captcha_output); ``` ### Yandex SmartCaptcha ```javascript import { CaptchaClient, Tasks } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); const task = new Tasks.YandexSmartCaptchaTaskProxyless({ websiteURL: 'https://example.com/login', websiteKey: 'FEXfAbHQsToo97VidNVk3j4dC74nGW1DgdxK4OoR' }); -const result = await client.solve(task); +const result = await captchaSolver.solve(task); console.log(result.token); ``` Use `Tasks.YandexSmartCaptchaTask` instead for the with-proxy variant (same extra @@ -193,35 +193,35 @@ the "Yandex SmartCaptcha image mode" section in import { CaptchaClient, Tasks } from 'captcha-sdk'; import fs from 'fs'; const body = fs.readFileSync('captcha.png').toString('base64'); -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); const task = new Tasks.CoordinatesTask({ body: body, comment: 'click on the green apple' }); -const result = await client.solve(task); +const result = await captchaSolver.solve(task); console.log(result.coordinates); ``` ### Tencent ```javascript import { CaptchaClient, Tasks } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); const task = new Tasks.TencentTaskProxyless({ websiteURL: 'https://example.com/login', appId: '190014885' }); -const result = await client.solve(task); +const result = await captchaSolver.solve(task); console.log(result.ticket); ``` ### Check balance ```javascript import { CaptchaClient } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: 'your_api_key' }); -const balance = await client.getBalance(); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); +const balance = await captchaSolver.getBalance(); console.log(`Balance: ${balance}`); ``` ### Custom timeout and polling ```javascript -const client = new CaptchaClient({ +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key', timeout: 180000, pollingInterval: 5000 @@ -230,9 +230,9 @@ const client = new CaptchaClient({ ### Error handling ```javascript import { CaptchaClient, ApiError, TimeoutError, NetworkError, ValidationError } from 'captcha-sdk'; -const client = new CaptchaClient({ clientKey: 'your_api_key' }); +const captchaSolver = new CaptchaClient({ clientKey: 'your_api_key' }); try { - const result = await client.solve(task); + const result = await captchaSolver.solve(task); } catch (error) { if (error instanceof ValidationError) { console.log(`Invalid input: ${error.message}`); diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..04a88ef --- /dev/null +++ b/examples/README.md @@ -0,0 +1,206 @@ +# Examples + +Runnable scripts for every captcha type the SDK supports. Each file is standalone: it builds a +`CaptchaClient`, creates one or two tasks, waits for the solution and prints it. Nothing here is +imported by the library itself — the directory exists to be read and executed. + +The same 11 scenarios are provided twice, once per calling style: + +| Suite | Style | Look here if | +|---|---|---| +| [async/](async) | `async/await` | your code is `await`-based (the common case) | +| [sync/](sync) | `.then()/.catch()` | you chain promises instead of awaiting them | + +Both suites call the same `CaptchaClient`. The SDK exposes a single promise-based API, and the split +only demonstrates two ways of consuming it — JavaScript has no blocking HTTP client, so there is no +true synchronous variant the way the Python SDK has `requests` vs. `httpx`. + +## Table of contents + +- [Setup](#setup) +- [Running an example](#running-an-example) +- [Example index](#example-index) +- [What each example covers](#what-each-example-covers) +- [Before you run](#before-you-run) + +## Setup + +**1. Install dependencies.** The scripts use `dotenv`, which is a devDependency of the repo: + +```bash +npm install +``` + +**2. Provide an API key.** Copy the template and fill in your key: + +```bash +cp .env.example .env +``` + +``` +CAPTCHA_API_KEY=your_api_key_here +``` + +Every script starts with `import 'dotenv/config'` and reads `process.env.CAPTCHA_API_KEY`, falling +back to the literal `'YOUR_API_KEY'` when the variable is missing. That fallback keeps the script +importable, but the API will reject it — set a real key before expecting results. + +**3. Node.js 18 or newer.** The examples rely on global `fetch` and, in `async/`, on top-level +`await`. + +> The scripts import the SDK by relative path (`../../src/index.js`) because they live inside this +> repository. In your own project, import it by package name instead: +> `import { CaptchaClient, Tasks } from 'captcha-sdk';` + +## Running an example + +Run from the repository root: + +```bash +node examples/async/balance.js +node examples/sync/recaptcha_v2.js +``` + +`balance.js` is the one file that works as-is — it only needs a valid key. Every other example ships +with placeholder values (`https://example.com/login`, `6Le-xxxxxxxxx`, …) that you must replace with +values from your own target page. See [Before you run](#before-you-run). + +Most files contain **two independent blocks** — a proxyless one and a with-proxy one — that both run +on execution. Comment out the block you don't need, or the script will submit two tasks and spend +twice the balance. + +## Example index + +| Captcha type | async | sync | Task classes | Solution fields | +|---|---|---|---|---| +| reCAPTCHA v2 | [recaptcha_v2.js](async/recaptcha_v2.js) | [recaptcha_v2.js](sync/recaptcha_v2.js) | `RecaptchaV2Proxyless`, `RecaptchaV2` | `gRecaptchaResponse` | +| reCAPTCHA v2 Enterprise | [recaptcha_v2_enterprise.js](async/recaptcha_v2_enterprise.js) | [recaptcha_v2_enterprise.js](sync/recaptcha_v2_enterprise.js) | `RecaptchaV2EnterpriseProxyless`, `RecaptchaV2Enterprise` | `gRecaptchaResponse` | +| reCAPTCHA v3 | [recaptcha_v3.js](async/recaptcha_v3.js) | [recaptcha_v3.js](sync/recaptcha_v3.js) | `RecaptchaV3Proxyless` | `gRecaptchaResponse` | +| Cloudflare Turnstile | [turnstile.js](async/turnstile.js) | [turnstile.js](sync/turnstile.js) | `TurnstileProxyless`, `Turnstile` | `token` | +| GeeTest v3 | [geetest_v3.js](async/geetest_v3.js) | [geetest_v3.js](sync/geetest_v3.js) | `GeeTestProxyless`, `GeeTest` | `challenge`, `validate`, `seccode` | +| GeeTest v4 | [geetest_v4.js](async/geetest_v4.js) | [geetest_v4.js](sync/geetest_v4.js) | `GeeTestProxyless`, `GeeTest` | `captcha_id`, `lot_number`, `pass_token`, `gen_time`, `captcha_output` | +| Yandex SmartCaptcha | [yandex_smartcaptcha.js](async/yandex_smartcaptcha.js) | [yandex_smartcaptcha.js](sync/yandex_smartcaptcha.js) | `YandexSmartCaptchaTaskProxyless`, `YandexSmartCaptchaTask` | `token` | +| Tencent | [tencent.js](async/tencent.js) | [tencent.js](sync/tencent.js) | `TencentTaskProxyless`, `TencentTask` | `appid`, `ret`, `ticket`, `randstr` | +| Image to Text | [image_to_text.js](async/image_to_text.js) | [image_to_text.js](sync/image_to_text.js) | `ImageToText` | `text` | +| Coordinates (click) | [coordinates.js](async/coordinates.js) | [coordinates.js](sync/coordinates.js) | `CoordinatesTask` | `coordinates` | +| Account balance | [balance.js](async/balance.js) | [balance.js](sync/balance.js) | — (`getBalance()`) | number | + +## What each example covers + +### reCAPTCHA v2 + +The baseline example, and the best one to read first. Shows the two shapes every proxy-capable type +follows: `RecaptchaV2Proxyless` (the service uses its own IPs) and `RecaptchaV2` with the five proxy +fields — `proxyType`, `proxyAddress`, `proxyPort`, `proxyLogin`, `proxyPassword`. Also documents the +two optional client settings, `timeout` and `pollingInterval`, and the `isInvisible` flag for +invisible widgets. + +### reCAPTCHA v2 Enterprise + +Same two shapes as v2, with `enterprisePayload` added. If the target site passes extra parameters to +`grecaptcha.enterprise.render()`, they must be forwarded in that object — otherwise the returned +token is rejected by the site even though the API call succeeded. + +### reCAPTCHA v3 + +Proxyless only; v3 has no with-proxy variant. `minScore` is required and drives cost and duration: +`0.3` is fastest, `0.7` balanced, `0.9` highest and slowest. `pageAction` should match the action the +site sets in `grecaptcha.execute()` — passing it raises the chance the token is accepted. The client +is created with `timeout: 180000` because v3 tasks run longer than v2. Commented-out lines show +`isEnterprise` and `apiDomain`. + +### Cloudflare Turnstile + +Proxyless and with-proxy variants. The important detail is in the comments: the token is tied to the +User-Agent, so if you pass `userAgent` you must reuse the exact same one when submitting the token. +For Cloudflare Challenge pages, `action`, `data` (the `data-cdata` attribute) and `pageData` (the +`chlPageData` parameter) also have to be extracted from the page and passed along. + +### GeeTest v3 + +The only example that performs a **request of its own before solving**. The `challenge` value is +session-specific and must be fresh for every task, so the script fetches one first and then builds +the task. The fetch URL is a deliberate placeholder — see [Before you run](#before-you-run). Uses +`timeout: 300000` and `pollingInterval: 10000`, since GeeTest is among the slowest types. `version` +is omitted because v3 is the default. + +### GeeTest v4 + +Same task classes as v3, different identification: v4 drops `gt`/`challenge` entirely and identifies +the widget by `captcha_id` inside `initParameters`, with `version: 4` set explicitly. Solution shape +differs from v3 as well — `captcha_output` and friends instead of `validate`/`seccode`. + +### Yandex SmartCaptcha + +Covers the **token** challenge, proxyless and with proxy. `websiteKey` is the sitekey from the page +source or the captcha iframe; `userAgent` and `cookies` are optional. Yandex's **image** challenge is +a different task type — it lives in the coordinates example below. + +### Tencent + +Proxyless and with proxy. `appId` is read from the page source. `captchaScript` is only needed when +the site loads the widget from a non-default script URL. + +### Image to Text + +Three blocks, no proxy variant. *Basic* submits the base64 image with the character-set hints that +match the shipped sample — letters, no digits. *Advanced* adds the rest of the hints that speed up +recognition — `phrase`, `math`, `comment` and an `imgInstructions` image — and is commented out for +want of that second image. *With language pool* shows that `languagePool` is the **second argument to +`solve()`**, not a task field: `solve(task, 'en')` picks an English-speaking worker pool (`'en'` or +`'ru'`). + +### Coordinates (click captcha) + +Three blocks, no proxy variant. *Basic* passes the shipped grid captcha plus a `comment` telling the +worker what to click — it repeats the instruction printed on the image itself, since nothing +guarantees the worker reads that. *Advanced* adds an `imgInstructions` image and the +`minClicks`/`maxClicks` limits. *Yandex SmartCaptcha image mode* reuses `CoordinatesTask` with +`imgType: 'smart_captcha'` (object selection) or `'pazl_smart_captcha'` (puzzle) — this is how you +solve Yandex's image challenge rather than its token challenge. The last two are commented out: both +need an instruction image the repository does not ship. + +### Account balance + +The shortest script: one `getBalance()` call returning the available amount as a number. It needs no +target page and no placeholder edits, which makes it the fastest way to confirm that your key and +network access work. + +## Before you run + +Every example except `balance.js` needs something replaced first. + +| Example | What you must supply | +|---|---| +| `balance.js` | Nothing — runs as-is with a valid key | +| `recaptcha_v2.js`, `recaptcha_v2_enterprise.js`, `recaptcha_v3.js` | Real `websiteURL` and `websiteKey` | +| `turnstile.js` | Real `websiteURL` and `websiteKey`; for Challenge pages also `action`, `data`, `pageData` | +| `yandex_smartcaptcha.js` | Real `websiteURL` and `websiteKey` | +| `tencent.js` | Real `websiteURL` and `appId` | +| `geetest_v3.js` | Real `websiteURL` and `gt`, **plus a real init endpoint** (see below) | +| `geetest_v4.js` | Real `websiteURL` and `captcha_id` | +| `image_to_text.js` | Nothing — the sample image ships in [assets/](assets/) | +| `coordinates.js` | Nothing — the sample image ships in [assets/](assets/) | + +**The two image examples run as-is**, with a valid key and nothing else. They read +[assets/text-captcha.png](assets/text-captcha.png) and +[assets/coordinates-captcha.png](assets/coordinates-captcha.png) through +`new URL('../assets/…', import.meta.url)`, which resolves against the script rather than the working +directory — so `node examples/async/coordinates.js` works from the repository root just as well as +from anywhere else. Point the read at your own file to solve a different image. + +**One block in each is commented out**: *Advanced* in `image_to_text.js`, and both *Advanced* and +*Yandex SmartCaptcha image mode* in `coordinates.js`. All three need a second picture — the +instruction image passed as `imgInstructions` — and the repository does not ship one yet. Drop your +own into [assets/](assets/) under the name the block expects and uncomment it. + +**`geetest_v3.js` will not run end-to-end as-is.** It fetches +`https://target-site.com/path/to/geetest/init`, which is a placeholder, not a live endpoint; the +script exits with code 1 when that fetch fails. Point it at your real target page — or wherever it +exposes a fresh `challenge` — before running. The block is there to show *where* that fetch belongs +in the flow. + +Proxy credentials (`1.2.3.4:8080`, `user`/`password`) are placeholders too. If you only want the +proxyless path, comment out the with-proxy block rather than leaving it to fail. + +Full API reference: https://captcha-solver.com/en/docs/captcha-types diff --git a/examples/assets/coordinates-captcha.png b/examples/assets/coordinates-captcha.png new file mode 100644 index 0000000..7b01414 Binary files /dev/null and b/examples/assets/coordinates-captcha.png differ diff --git a/examples/assets/text-captcha.png b/examples/assets/text-captcha.png new file mode 100644 index 0000000..e9d6a8a Binary files /dev/null and b/examples/assets/text-captcha.png differ diff --git a/examples/async/README.md b/examples/async/README.md new file mode 100644 index 0000000..d613bdf --- /dev/null +++ b/examples/async/README.md @@ -0,0 +1,157 @@ +# Examples — async/await + +The 11 SDK scenarios written in `async/await` style. Every file uses **top-level `await`** (possible +because the package is ESM, `"type": "module"`) and wraps each solve in its own `try/catch`, so a +failing block prints the error and the next block still runs. + +Setup, API key and the general gotchas are documented once in the [parent README](../README.md) — +read that first if you haven't. The identical scenarios in promise-chain style live in +[../sync/](../sync). + +Because every block is awaited at the top level, the blocks in a file execute **sequentially**: a +file with a proxyless block and a with-proxy block submits two tasks, one after the other, and +spends balance twice. Comment out the one you don't need. + +## Table of contents + +- [File index](#file-index) +- [What each file does](#what-each-file-does) + +## File index + +| File | What it does | Blocks in the file | +|---|---|---| +| [balance.js](balance.js) | Reads the account balance | single call | +| [recaptcha_v2.js](recaptcha_v2.js) | Solves reCAPTCHA v2 | Proxyless · With proxy | +| [recaptcha_v2_enterprise.js](recaptcha_v2_enterprise.js) | Solves reCAPTCHA v2 Enterprise | Proxyless · With proxy | +| [recaptcha_v3.js](recaptcha_v3.js) | Solves reCAPTCHA v3 with a score threshold | single block | +| [turnstile.js](turnstile.js) | Solves Cloudflare Turnstile | Proxyless · With proxy | +| [geetest_v3.js](geetest_v3.js) | Fetches a fresh `challenge`, then solves GeeTest v3 | Fetch · Proxyless · With proxy | +| [geetest_v4.js](geetest_v4.js) | Solves GeeTest v4 via `captcha_id` | Proxyless · With proxy | +| [yandex_smartcaptcha.js](yandex_smartcaptcha.js) | Solves Yandex SmartCaptcha (token challenge) | Proxyless · With proxy | +| [tencent.js](tencent.js) | Solves a Tencent captcha | Proxyless · With proxy | +| [image_to_text.js](image_to_text.js) | Recognises text on a captcha image | Basic · Advanced · With language pool | +| [coordinates.js](coordinates.js) | Solves a click captcha by coordinates | Basic · Advanced · Yandex image mode | + +## What each file does + +### balance.js + +[Source code](balance.js) · [API documentation](https://captcha-solver.com/en/docs/methods#post-getbalance) + +```javascript +const balance = await captchaSolver.getBalance(); +``` + +One call, returning the available amount as a number. No task, no target page, no placeholders — +the only file here that runs correctly without editing anything, which makes it the quickest check +that your key works. + +### recaptcha_v2.js + +[Source code](recaptcha_v2.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#recaptcha-v2) + +The reference file for the proxyless/with-proxy pattern that most other examples repeat. +`RecaptchaV2Proxyless` solves through the service's own IPs; `RecaptchaV2` adds `proxyType`, +`proxyAddress`, `proxyPort`, `proxyLogin` and `proxyPassword`. The client constructor is annotated +with the two optional settings — `timeout` (default 120000 ms) and `pollingInterval` (default +2000 ms) — and the task shows `isInvisible` for invisible widgets. Solution: `gRecaptchaResponse`. + +### recaptcha_v2_enterprise.js + +[Source code](recaptcha_v2_enterprise.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#recaptcha-v2-enterprise) + +Same two blocks, plus `enterprisePayload`. Enterprise widgets are rendered through +`grecaptcha.enterprise.render()`, and any extra parameters the site passes there must be forwarded +in that object — omit them and the site rejects an otherwise valid token. Solution: +`gRecaptchaResponse`. + +### recaptcha_v3.js + +[Source code](recaptcha_v3.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#recaptcha-v3) + +A single block: v3 has no with-proxy variant. `minScore` is required and controls how hard the task +is — `0.3` fastest, `0.7` balanced, `0.9` highest and slowest — so the client is created with +`timeout: 180000`. `pageAction` should mirror the action the site sets in `grecaptcha.execute()`. +Commented-out lines show `isEnterprise` and `apiDomain` for sites loading from `recaptcha.net`. +Solution: `gRecaptchaResponse`. + +### turnstile.js + +[Source code](turnstile.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#cloudflare-turnstile) + +Proxyless and with-proxy blocks. The commented optional fields matter more here than elsewhere: +`action`, `data` (`data-cdata`) and `pageData` (`chlPageData`) are required for Cloudflare Challenge +pages. Note the User-Agent rule spelled out in the comments — the token is bound to it, so if you +pass `userAgent`, the browser or bot submitting the token must send the same one. Solution: `token`. + +### geetest_v3.js + +[Source code](geetest_v3.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#geetest-v3) + +The only file that makes a request of its own before solving. `challenge` is session-specific and +must be fresh per task, so the script awaits a `fetch` to the target's init endpoint, destructures +`challenge` out of the JSON, and only then builds the task; if that fetch throws, it logs and calls +`process.exit(1)`. + +That URL — `https://target-site.com/path/to/geetest/init` — is a **placeholder**, so the file does +not run end-to-end unmodified. It marks where the fetch belongs in the flow; point it at your real +source of `challenge` first. + +The client uses `timeout: 300000` and `pollingInterval: 10000`, GeeTest being among the slowest +types. `version` is omitted since v3 is the default. Solution: `challenge`, `validate`, `seccode`. + +### geetest_v4.js + +[Source code](geetest_v4.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#geetest-v4) + +Same `GeeTestProxyless`/`GeeTest` classes as v3, but v4 identifies the widget differently: no +`gt`, no `challenge`, instead `version: 4` and `captcha_id` inside `initParameters`. No pre-fetch is +needed. Same extended timeouts. Solution: `captcha_id`, `lot_number`, `pass_token`, `gen_time`, +`captcha_output`. + +### yandex_smartcaptcha.js + +[Source code](yandex_smartcaptcha.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#yandex-smartcaptcha) + +The **token** challenge, proxyless and with proxy. `websiteKey` is the sitekey from the page source +or the captcha iframe; `userAgent` and `cookies` are optional. For Yandex's **image** challenge use +[coordinates.js](coordinates.js) instead — different task class entirely. Solution: `token`. + +### tencent.js + +[Source code](tencent.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#tencent) + +Proxyless and with proxy. `appId` comes from the page source; `captchaScript` is only needed when +the site loads the widget from a non-default script URL. Solution: `appid`, `ret`, `ticket`, +`randstr`. + +### image_to_text.js + +[Source code](image_to_text.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#image-to-text) + +Three blocks, no proxy variant. *Basic* sends the base64 image with the character-set hints that +match the shipped sample — `numeric: 2` (letters), `minLength`/`maxLength`. *Advanced* adds the rest +— `phrase`, `math`, `comment`, and a `text-captcha-hint.png` passed as `imgInstructions` — and is +**commented out**, because that hint image is not in the repository yet. *With language pool* +demonstrates the one API detail that is easy to get wrong: `languagePool` is the **second argument to +`solve()`**, not a task field — `await captchaSolver.solve(task, 'en')`, accepting `'en'` or `'ru'`. + +Reads [../assets/text-captcha.png](../assets/text-captcha.png) via `new URL(..., import.meta.url)`, +so the path holds **regardless of the working directory** and the script runs as-is. Solution: +`text`. + +### coordinates.js + +[Source code](coordinates.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#coordinates) + +Three blocks, no proxy variant. *Basic* submits the shipped grid captcha plus a `comment` telling the +worker what to click. *Advanced* adds a `coordinates-captcha-instruction.png` as `imgInstructions` +and constrains the answer with `minClicks`/`maxClicks`. *Yandex SmartCaptcha image mode* reuses the +same `CoordinatesTask` with `imgType: 'smart_captcha'` (object selection) or `'pazl_smart_captcha'` +(puzzle), which is how the image variant of Yandex SmartCaptcha is solved. The last two are +**commented out**: both need that instruction image, which is not in the repository yet. + +Reads [../assets/coordinates-captcha.png](../assets/coordinates-captcha.png) via +`new URL(..., import.meta.url)`, so the path holds **regardless of the working directory** and the +script runs as-is. Solution: `coordinates`, an array of `{ x, y }` points. diff --git a/examples/async/balance.js b/examples/async/balance.js index 37f1dbf..8fa4989 100644 --- a/examples/async/balance.js +++ b/examples/async/balance.js @@ -11,12 +11,12 @@ import { CaptchaClient } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // Get the current account balance. // Returns a float with the available amount in your account currency. try { - const balance = await client.getBalance(); + const balance = await captchaSolver.getBalance(); console.log('Balance:', balance); } catch (error) { console.error(error); diff --git a/examples/async/coordinates.js b/examples/async/coordinates.js index 7868fbe..03a5f42 100644 --- a/examples/async/coordinates.js +++ b/examples/async/coordinates.js @@ -2,9 +2,10 @@ * Example: Solve a click-based image captcha using CoordinatesTask. * * Prerequisites: - * Set the CAPTCHA_API_KEY environment variable. - * Provide the captcha image as base64 in the body parameter. - * Use comment to tell the worker what to click on the image. + * Set the CAPTCHA_API_KEY environment variable. That is all: the sample + * captcha ships with the repository, in examples/assets/. + * Point the read below at your own file to solve a different image, and use + * comment to tell the worker what to click on it. * No proxy is required. The image is submitted directly to the service. */ @@ -14,20 +15,24 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // Read and encode the captcha image to base64. // The body must be a pure base64 string without the data:image/...;base64, prefix. -const body = fs.readFileSync('./captcha.png').toString('base64'); +// The path is resolved against this file rather than the working directory, so +// the example runs from anywhere -- including from the repository root. +const body = fs.readFileSync(new URL('../assets/coordinates-captcha.png', import.meta.url)).toString('base64'); // --- Basic example --- -// Solves a simple click-based captcha with a hint for the worker. +// Solves a simple click-based captcha with a hint for the worker. The comment +// repeats the instruction printed on the sample image, because nothing +// guarantees the worker reads the text baked into the picture. try { const task = new Tasks.CoordinatesTask({ - body: body, // Base64-encoded captcha image (required) - comment: 'click on the green apple' // Text hint for the worker + body: body, // Base64-encoded captcha image (required) + comment: 'click on all squares with street signs' // Text hint for the worker }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); // Solution contains { coordinates: [{ x: 358, y: 268 }] } console.log('result:', result); } catch (error) { @@ -36,7 +41,15 @@ try { // --- Advanced example --- // Solves a captcha with instruction image and click count limits. -const imgInstructions = fs.readFileSync('./instruction.png').toString('base64'); +// +// This block and the Yandex one below are commented out: both need a second +// picture -- the instruction image -- and the repository does not ship one yet. +// Put your own coordinates-captcha-instruction.png into examples/assets/ and +// uncomment. +/* +const imgInstructions = fs.readFileSync( + new URL('../assets/coordinates-captcha-instruction.png', import.meta.url) +).toString('base64'); try { const task = new Tasks.CoordinatesTask({ @@ -46,7 +59,7 @@ try { minClicks: 1, // Minimum number of clicks (default 1) maxClicks: 3 // Maximum number of clicks allowed }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); console.log('result:', result); } catch (error) { console.error(error); @@ -55,15 +68,18 @@ try { // --- Yandex SmartCaptcha image mode --- // CoordinatesTask also solves Yandex SmartCaptcha in image mode via imgType. try { - const yandexInstructions = fs.readFileSync('./instruction.png').toString('base64'); + const yandexInstructions = fs.readFileSync( + new URL('../assets/coordinates-captcha-instruction.png', import.meta.url) + ).toString('base64'); const task = new Tasks.CoordinatesTask({ body: body, imgType: 'smart_captcha', // smart_captcha for object selection, or pazl_smart_captcha for puzzle imgInstructions: yandexInstructions, // Required for smart_captcha comment: 'select objects in the order of the instruction' }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); console.log('result:', result); } catch (error) { console.error(error); } +*/ diff --git a/examples/async/geetest_v3.js b/examples/async/geetest_v3.js index e288ad3..38ecb24 100644 --- a/examples/async/geetest_v3.js +++ b/examples/async/geetest_v3.js @@ -18,7 +18,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; // GeeTest tasks may take longer. Increase timeout if needed. -const client = new CaptchaClient({ clientKey: apiKey, timeout: 300000, pollingInterval: 10000 }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey, timeout: 300000, pollingInterval: 10000 }); // Fetch a fresh challenge value from the target page. // In production, extract this from the page's initGeetest call or network requests. @@ -44,7 +44,7 @@ try { // initParameters: {...}, // Extra params from initGeetest call // userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); // Solution contains { challenge, validate, seccode } console.log('result:', result); } catch (error) { @@ -63,7 +63,7 @@ try { proxyLogin: 'user', // Login for proxy authorization (optional) proxyPassword: 'password' // Password for proxy authorization (optional) }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); console.log('result:', result); } catch (error) { console.error(error); diff --git a/examples/async/geetest_v4.js b/examples/async/geetest_v4.js index ffd81dc..de26576 100644 --- a/examples/async/geetest_v4.js +++ b/examples/async/geetest_v4.js @@ -13,7 +13,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; // GeeTest v4 tasks may take longer. Increase timeout if needed. -const client = new CaptchaClient({ clientKey: apiKey, timeout: 300000, pollingInterval: 10000 }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey, timeout: 300000, pollingInterval: 10000 }); // --- Proxyless example --- // v4 drops gt/challenge. The widget is identified by captcha_id inside initParameters. @@ -27,7 +27,7 @@ try { // Optional fields // userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); // Solution contains { captcha_id, lot_number, pass_token, gen_time, captcha_output } console.log('result:', result); } catch (error) { @@ -48,7 +48,7 @@ try { proxyLogin: 'user', // Login for proxy authorization (optional) proxyPassword: 'password' // Password for proxy authorization (optional) }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); console.log('result:', result); } catch (error) { console.error(error); diff --git a/examples/async/image_to_text.js b/examples/async/image_to_text.js index 75e8d56..6d3fb61 100644 --- a/examples/async/image_to_text.js +++ b/examples/async/image_to_text.js @@ -2,8 +2,9 @@ * Example: Solve an Image to Text challenge. * * Prerequisites: - * Set the CAPTCHA_API_KEY environment variable. - * Provide a captcha image file as base64 in the body parameter. + * Set the CAPTCHA_API_KEY environment variable. That is all: the sample + * captcha ships with the repository, in examples/assets/. + * Point the read below at your own file to solve a different image. * Use optional fields to give hints to the worker for faster solving. */ @@ -14,22 +15,25 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; // Image to Text tasks are usually fast. Default timeout is fine. -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // Read and encode the captcha image to base64. // The body must be a pure base64 string without the data:image/...;base64, prefix. -const body = fs.readFileSync('./captcha.png').toString('base64'); +// The path is resolved against this file rather than the working directory, so +// the example runs from anywhere -- including from the repository root. +const body = fs.readFileSync(new URL('../assets/text-captcha.png', import.meta.url)).toString('base64'); // --- Basic example --- -// Solves a simple image captcha with character set hints. +// Solves a simple image captcha with character set hints. The hints below match +// the sample image, which is letters and no digits -- adjust them for your own. try { const task = new Tasks.ImageToText({ body: body, // Base64-encoded image (required) - numeric: 1, // 1 = digits only + numeric: 2, // 2 = letters only minLength: 4, // Minimum expected answer length maxLength: 6 // Maximum expected answer length }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); // Solution contains { text: "aB3fX9" } console.log('result:', result); } catch (error) { @@ -38,7 +42,16 @@ try { // --- Advanced example --- // Solves a math captcha with comment and instruction image. -const imgInstructions = fs.readFileSync('./captcha_hint.png').toString('base64'); +// +// Commented out, because it needs a second picture -- the instruction image +// shown to the worker -- and the repository does not ship one yet. Put your own +// text-captcha-hint.png into examples/assets/ and uncomment the block. Note the +// sample image is not a math captcha, so `math: true` fits your own image, not +// this one. +/* +const imgInstructions = fs.readFileSync( + new URL('../assets/text-captcha-hint.png', import.meta.url) +).toString('base64'); try { const task = new Tasks.ImageToText({ @@ -53,11 +66,12 @@ try { comment: 'Enter the result of the equation', // Text hint for the worker imgInstructions: imgInstructions // Optional instruction image for the worker }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); console.log('result:', result); } catch (error) { console.error(error); } +*/ // --- With language pool --- // The languagePool parameter selects the worker pool by language. @@ -66,11 +80,11 @@ try { try { const task = new Tasks.ImageToText({ body: body, - numeric: 1, + numeric: 2, minLength: 4, maxLength: 6 }); - const result = await client.solve(task, 'en'); // Picks English-speaking worker pool + const result = await captchaSolver.solve(task, 'en'); // Picks English-speaking worker pool console.log('result:', result); } catch (error) { console.error(error); diff --git a/examples/async/recaptcha_v2.js b/examples/async/recaptcha_v2.js index c861ac4..3154a54 100644 --- a/examples/async/recaptcha_v2.js +++ b/examples/async/recaptcha_v2.js @@ -14,7 +14,7 @@ const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; // Create a solver instance with your API key. // Optional: timeout (ms to wait for solution, default 120000) // Optional: pollingInterval (ms between status checks, default 2000) -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- // Solves reCAPTCHA v2 without a proxy. The service uses its own IP addresses. @@ -24,7 +24,7 @@ try { websiteKey: '6Le-xxxxxxxxxxxxxxxxxxxxxxxxxxxx', // data-sitekey attribute value isInvisible: false // Set true for invisible reCAPTCHA }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); // Solution contains { gRecaptchaResponse: "03AGdBq..." } console.log('result:', result); } catch (error) { @@ -45,7 +45,7 @@ try { proxyLogin: 'user', // Login for proxy authorization (optional) proxyPassword: 'password' // Password for proxy authorization (optional) }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); console.log('result:', result); } catch (error) { console.error(error); diff --git a/examples/async/recaptcha_v2_enterprise.js b/examples/async/recaptcha_v2_enterprise.js index e629932..14d724e 100644 --- a/examples/async/recaptcha_v2_enterprise.js +++ b/examples/async/recaptcha_v2_enterprise.js @@ -12,7 +12,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- // Enterprise captchas are loaded via the reCAPTCHA Enterprise API. If the site @@ -29,7 +29,7 @@ try { // userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', // cookies: 'session=abc123; token=xyz789', }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); // Solution contains { gRecaptchaResponse: "03AGdBq..." } console.log('result:', result); } catch (error) { @@ -48,7 +48,7 @@ try { proxyPassword: 'password', isInvisible: false }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); console.log('result:', result); } catch (error) { console.error(error); diff --git a/examples/async/recaptcha_v3.js b/examples/async/recaptcha_v3.js index 04f1e69..1575f22 100644 --- a/examples/async/recaptcha_v3.js +++ b/examples/async/recaptcha_v3.js @@ -13,7 +13,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; // reCAPTCHA v3 tasks may take longer to solve. Increase timeout if needed. -const client = new CaptchaClient({ clientKey: apiKey, timeout: 180000 }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey, timeout: 180000 }); // reCAPTCHA v3 returns a score instead of a pass/fail challenge. // The higher the minScore you request, the harder and longer the task takes. @@ -28,7 +28,7 @@ try { // isEnterprise: true, // Set true for reCAPTCHA v3 Enterprise // apiDomain: 'www.recaptcha.net', // Set if site loads from recaptcha.net }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); // Solution contains { gRecaptchaResponse: "03AGdBq..." } console.log('result:', result); } catch (error) { diff --git a/examples/async/tencent.js b/examples/async/tencent.js index 90651b1..91f8dfb 100644 --- a/examples/async/tencent.js +++ b/examples/async/tencent.js @@ -12,7 +12,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- // The service's own proxies are used to solve the captcha. @@ -24,7 +24,7 @@ try { // Optional fields: // captchaScript: 'https://turing.captcha.qcloud.com/TCaptcha.js', }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); // Solution contains { appid, ret, ticket, randstr } // Pass all four values together into the page's captcha callback as-is. console.log('result:', result); @@ -45,7 +45,7 @@ try { proxyLogin: 'user', // Login for proxy authorization (optional) proxyPassword: 'password' // Password for proxy authorization (optional) }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); console.log('result:', result); } catch (error) { console.error(error); diff --git a/examples/async/turnstile.js b/examples/async/turnstile.js index 6083b44..7e4caef 100644 --- a/examples/async/turnstile.js +++ b/examples/async/turnstile.js @@ -12,7 +12,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- // The token is tied to the User-Agent. If you pass userAgent, use the same @@ -27,7 +27,7 @@ try { // pageData: 'chl-page-data-value', // Value of chlPageData parameter // userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...', }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); // Solution contains { token: "0.zxcv..." } console.log('result:', result); } catch (error) { @@ -50,7 +50,7 @@ try { // data: 'custom-cdata-value', // pageData: 'chl-page-data-value', }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); console.log('result:', result); } catch (error) { console.error(error); diff --git a/examples/async/yandex_smartcaptcha.js b/examples/async/yandex_smartcaptcha.js index 3893ddc..a83749e 100644 --- a/examples/async/yandex_smartcaptcha.js +++ b/examples/async/yandex_smartcaptcha.js @@ -11,7 +11,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- // The service's own proxies are used to solve the captcha. @@ -24,7 +24,7 @@ try { // userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', // cookies: 'session=abc123; token=xyz789', }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); // Solution contains { token: "dV9xNjYyNTU3NjkxO4k9OTQuNVMuMjkuMjM9..." } console.log('result:', result); } catch (error) { @@ -44,7 +44,7 @@ try { proxyLogin: 'user', // Login for proxy authorization (optional) proxyPassword: 'password' // Password for proxy authorization (optional) }); - const result = await client.solve(task); + const result = await captchaSolver.solve(task); console.log('result:', result); } catch (error) { console.error(error); diff --git a/examples/sync/README.md b/examples/sync/README.md new file mode 100644 index 0000000..087c088 --- /dev/null +++ b/examples/sync/README.md @@ -0,0 +1,168 @@ +# Examples — promise chains + +The same 11 SDK scenarios as [../async/](../async), written with `.then()/.catch()` instead of +`await`. Nothing about the SDK changes between the two suites: `CaptchaClient` returns promises, and +these files simply consume them without `await`. + +Setup, API key and the general gotchas are documented once in the [parent README](../README.md) — +read that first if you haven't. + +Two consequences of dropping `await` are worth knowing before you run anything here: + +- **Tasks are named, not inline.** Each block declares its task in a `const` (`proxylessTask`, + `proxyTask`, `basicTask`, …) and then passes it to `captchaSolver.solve(...)`, which keeps the + promise chain readable. +- **Blocks start concurrently.** In `async/`, a top-level `await` makes the proxyless block finish + before the with-proxy block begins. Here nothing blocks, so both `solve()` calls are submitted + almost simultaneously and their results arrive in whatever order the API returns them. Two tasks, + twice the balance — comment out the block you don't need. + +Errors are handled per chain with a trailing `.catch()`; a failing block does not stop the others. + +## Table of contents + +- [File index](#file-index) +- [What each file does](#what-each-file-does) + +## File index + +| File | What it does | Blocks in the file | +|---|---|---| +| [balance.js](balance.js) | Reads the account balance | single call | +| [recaptcha_v2.js](recaptcha_v2.js) | Solves reCAPTCHA v2 | Proxyless · With proxy | +| [recaptcha_v2_enterprise.js](recaptcha_v2_enterprise.js) | Solves reCAPTCHA v2 Enterprise | Proxyless · With proxy | +| [recaptcha_v3.js](recaptcha_v3.js) | Solves reCAPTCHA v3 with a score threshold | single block | +| [turnstile.js](turnstile.js) | Solves Cloudflare Turnstile | Proxyless · With proxy | +| [geetest_v3.js](geetest_v3.js) | Fetches a fresh `challenge`, then solves GeeTest v3 | one nested chain | +| [geetest_v4.js](geetest_v4.js) | Solves GeeTest v4 via `captcha_id` | Proxyless · With proxy | +| [yandex_smartcaptcha.js](yandex_smartcaptcha.js) | Solves Yandex SmartCaptcha (token challenge) | Proxyless · With proxy | +| [tencent.js](tencent.js) | Solves a Tencent captcha | Proxyless · With proxy | +| [image_to_text.js](image_to_text.js) | Recognises text on a captcha image | Basic · Advanced · With language pool | +| [coordinates.js](coordinates.js) | Solves a click captcha by coordinates | Basic · Advanced · Yandex image mode | + +## What each file does + +### balance.js + +[Source code](balance.js) · [API documentation](https://captcha-solver.com/en/docs/methods#post-getbalance) + +```javascript +captchaSolver.getBalance() + .then((balance) => console.log('Balance:', balance)) +``` + +One call resolving to the available amount as a number. No task, no target page, no placeholders — +the only file here that runs correctly without editing anything, and therefore the quickest check +that your key works. + +### recaptcha_v2.js + +[Source code](recaptcha_v2.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#recaptcha-v2) + +The reference file for the proxyless/with-proxy pattern most other examples repeat. +`RecaptchaV2Proxyless` solves through the service's own IPs; `RecaptchaV2` adds `proxyType`, +`proxyAddress`, `proxyPort`, `proxyLogin` and `proxyPassword`. The client constructor is annotated +with the two optional settings — `timeout` (default 120000 ms) and `pollingInterval` (default +2000 ms) — and the task shows `isInvisible` for invisible widgets. Solution: `gRecaptchaResponse`. + +### recaptcha_v2_enterprise.js + +[Source code](recaptcha_v2_enterprise.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#recaptcha-v2-enterprise) + +Same two chains, plus `enterprisePayload`. Enterprise widgets render through +`grecaptcha.enterprise.render()`, and any extra parameters the site passes there must be forwarded +in that object — omit them and the site rejects an otherwise valid token. Solution: +`gRecaptchaResponse`. + +### recaptcha_v3.js + +[Source code](recaptcha_v3.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#recaptcha-v3) + +A single chain: v3 has no with-proxy variant. `minScore` is required and controls how hard the task +is — `0.3` fastest, `0.7` balanced, `0.9` highest and slowest — so the client is created with +`timeout: 180000`. `pageAction` should mirror the action the site sets in `grecaptcha.execute()`. +Commented-out lines show `isEnterprise` and `apiDomain` for sites loading from `recaptcha.net`. +Solution: `gRecaptchaResponse`. + +### turnstile.js + +[Source code](turnstile.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#cloudflare-turnstile) + +Proxyless and with-proxy chains. The commented optional fields matter more here than elsewhere: +`action`, `data` (`data-cdata`) and `pageData` (`chlPageData`) are required for Cloudflare Challenge +pages. Note the User-Agent rule spelled out in the comments — the token is bound to it, so if you +pass `userAgent`, the browser or bot submitting the token must send the same one. Solution: `token`. + +### geetest_v3.js + +[Source code](geetest_v3.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#geetest-v3) + +Structurally the odd one out. `challenge` is session-specific and must be fresh per task, so the +file opens with a `fetch(...).then(response => response.json())` chain and **nests both solve blocks +inside** the `.then(({ challenge }) => { … })` callback — the tasks cannot be built until the value +arrives. This is the clearest place to see what `await` buys you: the async version expresses the +same dependency as three flat statements. + +That URL — `https://target-site.com/path/to/geetest/init` — is a **placeholder**, so the file does +not run end-to-end unmodified. It marks where the fetch belongs in the flow; point it at your real +source of `challenge` first. + +The client uses `timeout: 300000` and `pollingInterval: 10000`, GeeTest being among the slowest +types. `version` is omitted since v3 is the default. Solution: `challenge`, `validate`, `seccode`. + +### geetest_v4.js + +[Source code](geetest_v4.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#geetest-v4) + +Same `GeeTestProxyless`/`GeeTest` classes as v3, but v4 identifies the widget differently: no `gt`, +no `challenge`, instead `version: 4` and `captcha_id` inside `initParameters`. No pre-fetch is +needed, so this file keeps the flat two-chain layout. Same extended timeouts. Solution: +`captcha_id`, `lot_number`, `pass_token`, `gen_time`, `captcha_output`. + +### yandex_smartcaptcha.js + +[Source code](yandex_smartcaptcha.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#yandex-smartcaptcha) + +The **token** challenge, proxyless and with proxy. `websiteKey` is the sitekey from the page source +or the captcha iframe; `userAgent` and `cookies` are optional. For Yandex's **image** challenge use +[coordinates.js](coordinates.js) instead — different task class entirely. Solution: `token`. + +### tencent.js + +[Source code](tencent.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#tencent) + +Proxyless and with proxy. `appId` comes from the page source; `captchaScript` is only needed when +the site loads the widget from a non-default script URL. Solution: `appid`, `ret`, `ticket`, +`randstr`. + +### image_to_text.js + +[Source code](image_to_text.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#image-to-text) + +Three chains, no proxy variant. *Basic* (`basicTask`) sends the base64 image with the character-set +hints that match the shipped sample — `numeric: 2` (letters), `minLength`/`maxLength`. *Advanced* +(`advancedTask`) adds the rest — `phrase`, `math`, `comment`, and a `text-captcha-hint.png` passed as +`imgInstructions` — and is **commented out**, because that hint image is not in the repository yet. +*With language pool* (`languagePoolTask`) demonstrates the one API detail that is easy to get wrong: +`languagePool` is the **second argument to `solve()`**, not a task field — +`captchaSolver.solve(languagePoolTask, 'en')`, accepting `'en'` or `'ru'`. + +Reads [../assets/text-captcha.png](../assets/text-captcha.png) via `new URL(..., import.meta.url)`, +so the path holds **regardless of the working directory** and the script runs as-is. Solution: +`text`. + +### coordinates.js + +[Source code](coordinates.js) · [API documentation](https://captcha-solver.com/en/docs/captcha-types#coordinates) + +Three chains, no proxy variant. *Basic* (`basicTask`) submits the shipped grid captcha plus a +`comment` telling the worker what to click. *Advanced* (`advancedTask`) adds a +`coordinates-captcha-instruction.png` as `imgInstructions` and constrains the answer with +`minClicks`/`maxClicks`. *Yandex SmartCaptcha image mode* (`yandexTask`) reuses the same +`CoordinatesTask` with `imgType: 'smart_captcha'` (object selection) or `'pazl_smart_captcha'` +(puzzle), which is how the image variant of Yandex SmartCaptcha is solved. The last two are +**commented out**: both need that instruction image, which is not in the repository yet. + +Reads [../assets/coordinates-captcha.png](../assets/coordinates-captcha.png) via +`new URL(..., import.meta.url)`, so the path holds **regardless of the working directory** and the +script runs as-is. Solution: `coordinates`, an array of `{ x, y }` points. diff --git a/examples/sync/balance.js b/examples/sync/balance.js index 9b5d541..ba0675d 100644 --- a/examples/sync/balance.js +++ b/examples/sync/balance.js @@ -11,11 +11,11 @@ import { CaptchaClient } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // Get the current account balance. // Returns a float with the available amount in your account currency. -client.getBalance() +captchaSolver.getBalance() .then((balance) => { console.log('Balance:', balance); }) diff --git a/examples/sync/coordinates.js b/examples/sync/coordinates.js index 2f65b93..2412ac0 100644 --- a/examples/sync/coordinates.js +++ b/examples/sync/coordinates.js @@ -2,9 +2,10 @@ * Example: Solve a click-based image captcha using CoordinatesTask. * * Prerequisites: - * Set the CAPTCHA_API_KEY environment variable. - * Provide the captcha image as base64 in the body parameter. - * Use comment to tell the worker what to click on the image. + * Set the CAPTCHA_API_KEY environment variable. That is all: the sample + * captcha ships with the repository, in examples/assets/. + * Point the read below at your own file to solve a different image, and use + * comment to tell the worker what to click on it. * No proxy is required. The image is submitted directly to the service. */ @@ -14,20 +15,24 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // Read and encode the captcha image to base64. // The body must be a pure base64 string without the data:image/...;base64, prefix. -const body = fs.readFileSync('./captcha.png').toString('base64'); +// The path is resolved against this file rather than the working directory, so +// the example runs from anywhere -- including from the repository root. +const body = fs.readFileSync(new URL('../assets/coordinates-captcha.png', import.meta.url)).toString('base64'); // --- Basic example --- -// Solves a simple click-based captcha with a hint for the worker. +// Solves a simple click-based captcha with a hint for the worker. The comment +// repeats the instruction printed on the sample image, because nothing +// guarantees the worker reads the text baked into the picture. const basicTask = new Tasks.CoordinatesTask({ - body: body, // Base64-encoded captcha image (required) - comment: 'click on the green apple' // Text hint for the worker + body: body, // Base64-encoded captcha image (required) + comment: 'click on all squares with street signs' // Text hint for the worker }); -client.solve(basicTask) +captchaSolver.solve(basicTask) .then((result) => { // Solution contains { coordinates: [{ x: 358, y: 268 }] } console.log('result:', result); @@ -38,7 +43,15 @@ client.solve(basicTask) // --- Advanced example --- // Solves a captcha with instruction image and click count limits. -const imgInstructions = fs.readFileSync('./instruction.png').toString('base64'); +// +// This block and the Yandex one below are commented out: both need a second +// picture -- the instruction image -- and the repository does not ship one yet. +// Put your own coordinates-captcha-instruction.png into examples/assets/ and +// uncomment. +/* +const imgInstructions = fs.readFileSync( + new URL('../assets/coordinates-captcha-instruction.png', import.meta.url) +).toString('base64'); const advancedTask = new Tasks.CoordinatesTask({ body: body, // Base64-encoded captcha image @@ -48,7 +61,7 @@ const advancedTask = new Tasks.CoordinatesTask({ maxClicks: 3 // Maximum number of clicks allowed }); -client.solve(advancedTask) +captchaSolver.solve(advancedTask) .then((result) => { console.log('result:', result); }) @@ -65,10 +78,11 @@ const yandexTask = new Tasks.CoordinatesTask({ comment: 'select objects in the order of the instruction' }); -client.solve(yandexTask) +captchaSolver.solve(yandexTask) .then((result) => { console.log('result:', result); }) .catch((error) => { console.error(error); }); +*/ diff --git a/examples/sync/geetest_v3.js b/examples/sync/geetest_v3.js index aa501f8..5527bb2 100644 --- a/examples/sync/geetest_v3.js +++ b/examples/sync/geetest_v3.js @@ -18,7 +18,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; // GeeTest tasks may take longer. Increase timeout if needed. -const client = new CaptchaClient({ clientKey: apiKey, timeout: 300000, pollingInterval: 10000 }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey, timeout: 300000, pollingInterval: 10000 }); // Fetch a fresh challenge value from the target page. // In production, extract this from the page's initGeetest call or network requests. @@ -38,7 +38,7 @@ fetch('https://target-site.com/path/to/geetest/init') // userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', }); - client.solve(proxylessTask) + captchaSolver.solve(proxylessTask) .then((result) => { // Solution contains { challenge, validate, seccode } console.log('result:', result); @@ -59,7 +59,7 @@ fetch('https://target-site.com/path/to/geetest/init') proxyPassword: 'password' // Password for proxy authorization (optional) }); - client.solve(proxyTask) + captchaSolver.solve(proxyTask) .then((result) => { console.log('result:', result); }) diff --git a/examples/sync/geetest_v4.js b/examples/sync/geetest_v4.js index 30d599a..652f1b7 100644 --- a/examples/sync/geetest_v4.js +++ b/examples/sync/geetest_v4.js @@ -13,7 +13,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; // GeeTest v4 tasks may take longer. Increase timeout if needed. -const client = new CaptchaClient({ clientKey: apiKey, timeout: 300000, pollingInterval: 10000 }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey, timeout: 300000, pollingInterval: 10000 }); // --- Proxyless example --- // v4 drops gt/challenge. The widget is identified by captcha_id inside initParameters. @@ -27,7 +27,7 @@ const proxylessTask = new Tasks.GeeTestProxyless({ // userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', }); -client.solve(proxylessTask) +captchaSolver.solve(proxylessTask) .then((result) => { // Solution contains { captcha_id, lot_number, pass_token, gen_time, captcha_output } console.log('result:', result); @@ -50,7 +50,7 @@ const proxyTask = new Tasks.GeeTest({ proxyPassword: 'password' // Password for proxy authorization (optional) }); -client.solve(proxyTask) +captchaSolver.solve(proxyTask) .then((result) => { console.log('result:', result); }) diff --git a/examples/sync/image_to_text.js b/examples/sync/image_to_text.js index d9b252d..e1ea121 100644 --- a/examples/sync/image_to_text.js +++ b/examples/sync/image_to_text.js @@ -2,8 +2,9 @@ * Example: Solve an Image to Text challenge. * * Prerequisites: - * Set the CAPTCHA_API_KEY environment variable. - * Provide a captcha image file as base64 in the body parameter. + * Set the CAPTCHA_API_KEY environment variable. That is all: the sample + * captcha ships with the repository, in examples/assets/. + * Point the read below at your own file to solve a different image. * Use optional fields to give hints to the worker for faster solving. */ @@ -14,22 +15,25 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; // Image to Text tasks are usually fast. Default timeout is fine. -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // Read and encode the captcha image to base64. // The body must be a pure base64 string without the data:image/...;base64, prefix. -const body = fs.readFileSync('./captcha.png').toString('base64'); +// The path is resolved against this file rather than the working directory, so +// the example runs from anywhere -- including from the repository root. +const body = fs.readFileSync(new URL('../assets/text-captcha.png', import.meta.url)).toString('base64'); // --- Basic example --- -// Solves a simple image captcha with character set hints. +// Solves a simple image captcha with character set hints. The hints below match +// the sample image, which is letters and no digits -- adjust them for your own. const basicTask = new Tasks.ImageToText({ body: body, // Base64-encoded image (required) - numeric: 1, // 1 = digits only + numeric: 2, // 2 = letters only minLength: 4, // Minimum expected answer length maxLength: 6 // Maximum expected answer length }); -client.solve(basicTask) +captchaSolver.solve(basicTask) .then((result) => { // Solution contains { text: "aB3fX9" } console.log('result:', result); @@ -40,7 +44,16 @@ client.solve(basicTask) // --- Advanced example --- // Solves a math captcha with comment and instruction image. -const imgInstructions = fs.readFileSync('./captcha_hint.png').toString('base64'); +// +// Commented out, because it needs a second picture -- the instruction image +// shown to the worker -- and the repository does not ship one yet. Put your own +// text-captcha-hint.png into examples/assets/ and uncomment the block. Note the +// sample image is not a math captcha, so `math: true` fits your own image, not +// this one. +/* +const imgInstructions = fs.readFileSync( + new URL('../assets/text-captcha-hint.png', import.meta.url) +).toString('base64'); const advancedTask = new Tasks.ImageToText({ body: body, // Base64-encoded captcha image @@ -55,13 +68,14 @@ const advancedTask = new Tasks.ImageToText({ imgInstructions: imgInstructions // Optional instruction image for the worker }); -client.solve(advancedTask) +captchaSolver.solve(advancedTask) .then((result) => { console.log('result:', result); }) .catch((error) => { console.error(error); }); +*/ // --- With language pool --- // The languagePool parameter selects the worker pool by language. @@ -69,12 +83,12 @@ client.solve(advancedTask) // Accepted values: "en" (English) or "ru" (Russian). const languagePoolTask = new Tasks.ImageToText({ body: body, - numeric: 1, + numeric: 2, minLength: 4, maxLength: 6 }); -client.solve(languagePoolTask, 'en') // Picks English-speaking worker pool +captchaSolver.solve(languagePoolTask, 'en') // Picks English-speaking worker pool .then((result) => { console.log('result:', result); }) diff --git a/examples/sync/recaptcha_v2.js b/examples/sync/recaptcha_v2.js index 53bc03c..739509d 100644 --- a/examples/sync/recaptcha_v2.js +++ b/examples/sync/recaptcha_v2.js @@ -14,7 +14,7 @@ const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; // Create a solver instance with your API key. // Optional: timeout (ms to wait for solution, default 120000) // Optional: pollingInterval (ms between status checks, default 2000) -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- // Solves reCAPTCHA v2 without a proxy. The service uses its own IP addresses. @@ -24,7 +24,7 @@ const proxylessTask = new Tasks.RecaptchaV2Proxyless({ isInvisible: false // Set true for invisible reCAPTCHA }); -client.solve(proxylessTask) +captchaSolver.solve(proxylessTask) .then((result) => { // Solution contains { gRecaptchaResponse: "03AGdBq..." } console.log('result:', result); @@ -47,7 +47,7 @@ const proxyTask = new Tasks.RecaptchaV2({ proxyPassword: 'password' // Password for proxy authorization (optional) }); -client.solve(proxyTask) +captchaSolver.solve(proxyTask) .then((result) => { console.log('result:', result); }) diff --git a/examples/sync/recaptcha_v2_enterprise.js b/examples/sync/recaptcha_v2_enterprise.js index cd5d4ef..e631c1a 100644 --- a/examples/sync/recaptcha_v2_enterprise.js +++ b/examples/sync/recaptcha_v2_enterprise.js @@ -12,7 +12,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- // Enterprise captchas are loaded via the reCAPTCHA Enterprise API. If the site @@ -29,7 +29,7 @@ const proxylessTask = new Tasks.RecaptchaV2EnterpriseProxyless({ // cookies: 'session=abc123; token=xyz789', }); -client.solve(proxylessTask) +captchaSolver.solve(proxylessTask) .then((result) => { // Solution contains { gRecaptchaResponse: "03AGdBq..." } console.log('result:', result); @@ -50,7 +50,7 @@ const proxyTask = new Tasks.RecaptchaV2Enterprise({ isInvisible: false }); -client.solve(proxyTask) +captchaSolver.solve(proxyTask) .then((result) => { console.log('result:', result); }) diff --git a/examples/sync/recaptcha_v3.js b/examples/sync/recaptcha_v3.js index 811cf0d..bf5057f 100644 --- a/examples/sync/recaptcha_v3.js +++ b/examples/sync/recaptcha_v3.js @@ -13,7 +13,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; // reCAPTCHA v3 tasks may take longer to solve. Increase timeout if needed. -const client = new CaptchaClient({ clientKey: apiKey, timeout: 180000 }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey, timeout: 180000 }); // reCAPTCHA v3 returns a score instead of a pass/fail challenge. // The higher the minScore you request, the harder and longer the task takes. @@ -28,7 +28,7 @@ const task = new Tasks.RecaptchaV3Proxyless({ // apiDomain: 'www.recaptcha.net', // Set if site loads from recaptcha.net }); -client.solve(task) +captchaSolver.solve(task) .then((result) => { // Solution contains { gRecaptchaResponse: "03AGdBq..." } console.log('result:', result); diff --git a/examples/sync/tencent.js b/examples/sync/tencent.js index f63fad1..ce58b31 100644 --- a/examples/sync/tencent.js +++ b/examples/sync/tencent.js @@ -12,7 +12,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- // The service's own proxies are used to solve the captcha. @@ -24,7 +24,7 @@ const proxylessTask = new Tasks.TencentTaskProxyless({ // captchaScript: 'https://turing.captcha.qcloud.com/TCaptcha.js', }); -client.solve(proxylessTask) +captchaSolver.solve(proxylessTask) .then((result) => { // Solution contains { appid, ret, ticket, randstr } // Pass all four values together into the page's captcha callback as-is. @@ -47,7 +47,7 @@ const proxyTask = new Tasks.TencentTask({ proxyPassword: 'password' // Password for proxy authorization (optional) }); -client.solve(proxyTask) +captchaSolver.solve(proxyTask) .then((result) => { console.log('result:', result); }) diff --git a/examples/sync/turnstile.js b/examples/sync/turnstile.js index 99bc939..35a5855 100644 --- a/examples/sync/turnstile.js +++ b/examples/sync/turnstile.js @@ -12,7 +12,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- // The token is tied to the User-Agent. If you pass userAgent, use the same @@ -27,7 +27,7 @@ const proxylessTask = new Tasks.TurnstileProxyless({ // userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...', }); -client.solve(proxylessTask) +captchaSolver.solve(proxylessTask) .then((result) => { // Solution contains { token: "0.zxcv..." } console.log('result:', result); @@ -52,7 +52,7 @@ const proxyTask = new Tasks.Turnstile({ // pageData: 'chl-page-data-value', }); -client.solve(proxyTask) +captchaSolver.solve(proxyTask) .then((result) => { console.log('result:', result); }) diff --git a/examples/sync/yandex_smartcaptcha.js b/examples/sync/yandex_smartcaptcha.js index af270e0..bd16021 100644 --- a/examples/sync/yandex_smartcaptcha.js +++ b/examples/sync/yandex_smartcaptcha.js @@ -11,7 +11,7 @@ import { CaptchaClient, Tasks } from '../../src/index.js'; const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; -const client = new CaptchaClient({ clientKey: apiKey }); +const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- // The service's own proxies are used to solve the captcha. @@ -24,7 +24,7 @@ const proxylessTask = new Tasks.YandexSmartCaptchaTaskProxyless({ // cookies: 'session=abc123; token=xyz789', }); -client.solve(proxylessTask) +captchaSolver.solve(proxylessTask) .then((result) => { // Solution contains { token: "dV9xNjYyNTU3NjkxO4k9OTQuNVMuMjkuMjM9..." } console.log('result:', result); @@ -46,7 +46,7 @@ const proxyTask = new Tasks.YandexSmartCaptchaTask({ proxyPassword: 'password' // Password for proxy authorization (optional) }); -client.solve(proxyTask) +captchaSolver.solve(proxyTask) .then((result) => { console.log('result:', result); }) diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..adf4568 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,18 @@ +/** + * Jest-конфигурация. Файл в ESM-синтаксисе, потому что в package.json + * задано "type": "module". + * + * Запуск требует флага --experimental-vm-modules (см. скрипты в package.json). + */ + +export default { + testEnvironment: 'node', + + // Считать покрытие по всем файлам src/, а не только по тем, которые + // импортированы из тестов. Иначе новый файл, который никто не подключил, + // молча выпадет из отчёта вместо того, чтобы показать 0%. + collectCoverageFrom: ['src/**/*.js'], + + // text -- для вывода в консоль, lcov -- для выгрузки в Coveralls. + coverageReporters: ['text', 'lcov'] +}; diff --git a/package.json b/package.json index fc621e2..3f3f188 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "captcha-sdk", - "version": "1.1.0", + "version": "0.0.1", "description": "Official JavaScript SDK for the Captcha Solver API. Solve reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest, Yandex SmartCaptcha, Tencent, and image/click captchas.", "main": "src/index.js", "type": "module", @@ -14,8 +14,8 @@ ], "scripts": { "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", - "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathIgnorePatterns=integration", - "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=integration" + "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js tests/unit", + "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js tests/integration" }, "keywords": [ "captcha-solver", diff --git a/src/index.js b/src/index.js index 53406cc..6c5a3e4 100644 --- a/src/index.js +++ b/src/index.js @@ -2,4 +2,4 @@ export { CaptchaClient } from './client.js'; export { ApiError, NetworkError, TimeoutError, ValidationError, CaptchaError } from './exceptions.js'; export * as Tasks from './tasks.js'; -export const __version__ = '1.1.0'; \ No newline at end of file +export const __version__ = '0.0.1'; \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..64ea8a5 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,455 @@ +# SDK tests + +Documentation for the `captcha-sdk` test suite: what is covered, how to run it by hand, and what runs in CI. + +## Contents + +- [Directory layout](#directory-layout) +- [Test types](#test-types) +- [How the tests import the SDK](#how-the-tests-import-the-sdk) +- [Unit tests](#unit-tests) + - [How sync and async are split](#how-sync-and-async-are-split) + - [Client tests](#client-tests-clienttestjs) + - [Tests per captcha type](#tests-per-captcha-type) + - [Public API contract](#public-api-contract) +- [Integration tests](#integration-tests) + - [Why the repository carries no targets](#why-the-repository-carries-no-targets) + - [What is not covered here](#what-is-not-covered-here) + - [The shared helper](#the-shared-helper) + - [How they switch on](#how-they-switch-on) +- [Running them by hand](#running-them-by-hand) + - [Requirements](#requirements) +- [Coverage](#coverage) +- [CI / pipeline](#ci--pipeline) + +## Directory layout + +The top level splits tests by **type**, the nested level by **calling style**: + +``` +tests/ +├── unit/ # no network, fetch is mocked, no key needed +│ ├── public-api.test.js # contract of the package's public entry points +│ ├── sync/ # promise-chain style (.then/.catch) +│ │ ├── client.test.js +│ │ ├── coordinates.test.js +│ │ ├── geetest.test.js +│ │ ├── image_to_text.test.js +│ │ ├── recaptcha_v2.test.js +│ │ ├── recaptcha_v2_enterprise.test.js +│ │ ├── recaptcha_v3.test.js +│ │ ├── tencent.test.js +│ │ ├── turnstile.test.js +│ │ └── yandex_smartcaptcha.test.js +│ └── async/ # the same scenarios in async/await style +│ └── (the same 10 files) +└── integration/ # against the real API (needs a key, spends balance) + ├── helpers.js # shared guard and client factory (not a test) + ├── balance.test.js # free check: balance + ├── image_to_text.test.js # paid checks: real solve(), one file per type + ├── coordinates.test.js + ├── recaptcha_v2.test.js + ├── recaptcha_v3.test.js + ├── turnstile.test.js + ├── geetest_v4.test.js + ├── yandex_smartcaptcha.test.js + └── tencent.test.js +``` + +The runner is Jest 29 in ESM mode (`node --experimental-vm-modules`), configured in [jest.config.js](../jest.config.js) at the project root. The default `testMatch` is used, so any `*.test.js` file is picked up. Suites are selected by directory path (`jest tests/unit` / `jest tests/integration`), so a new file needs no configuration — its location alone puts it in the right set. + +## Test types + +| Type | Where | Network | API key needed | Count | +|---|---|---|---|---| +| Unit | `tests/unit/` | no, `fetch` is mocked | no | 21 suites / 81 tests | +| Integration | `tests/integration/` | yes, the real API | yes | 9 suites / 9 tests | + +Unit tests are fully isolated: the network layer is replaced either through `global.fetch = jest.fn(...)` or through `jest.spyOn(client, '_request')`. No outbound requests, no charges against the account. + +## How the tests import the SDK + +Every test pulls in the SDK **by package name**, the way a user would, rather than by direct file paths: + +```javascript +import { CaptchaClient, Tasks } from 'captcha-sdk'; +``` + +This works with no extra setup: Node supports self-reference — a package can import itself by name as long as `package.json` has a `name` and an `exports` map. Jest's resolver honours that, including the ban on undeclared subpaths. + +Why this instead of `../../../src/client.js`: + +- **The tests check what the user actually gets.** Direct imports bypass [src/index.js](../src/index.js) and the `exports` map. With them you could delete an export from `index.js` or break `exports` in `package.json` and the whole suite would stay green, even though the package would work for nobody. +- **Coverage becomes honest.** With direct imports, `index.js` was loaded by no test at all and simply dropped out of Jest's report — coverage read 97.5 % while saying nothing about an entirely unchecked entry point. +- **No brittle `../../../`.** Moving directories means editing `jest.config.js`, not 21 files. + +Mocking does not depend on the import style: `global.fetch` is replaced globally, and `jest.spyOn(client, '_request')` works at the instance level. + +## Unit tests + +### How sync and async are split + +The SDK exposes one and the same promise-based API, usable in two styles. The subdirectories reflect the calling style, not two different implementations: + +- `tests/unit/sync/` — calls via `.then()/.catch()`, the test returns a promise; +- `tests/unit/async/` — the same calls via `async/await`. + +To avoid duplicating checks, the rule is: + +- **Task serialization** (`task.toDict()`) is checked **once**, in `tests/unit/sync/.test.js`. It does not depend on the calling style. +- **`solve()`** is checked in **both** directories — that is the whole point of the split: confirming that the SDK's promises behave correctly in either style. + +That is why the files under `async/` are noticeably shorter: they only hold `solve()`. + +### Client tests (`client.test.js`) + +`unit/sync/client.test.js` and `unit/async/client.test.js` mirror each other, 12 tests apiece. They cover transport, error handling and polling, independently of any particular captcha type: + +| Test | What it checks | +|---|---| +| `throws ValidationError when clientKey is missing` | the `CaptchaClient` constructor without `clientKey` throws `ValidationError` | +| `creates task and returns taskId` | `createTask()` returns the `taskId` from the API response | +| `sends languagePool when provided` | when `languagePool` is passed, the field goes out in the request body | +| `does not send languagePool when not provided` | without `languagePool` the field is **absent** from the body | +| `getTaskResult returns full API response` | `getTaskResult()` hands back the whole response (`status`, `solution`), not just the solution | +| `getBalance returns balance as float` | the API's `"10.50"` string is coerced to the number `10.5` | +| `solve polls until status is ready and returns solution` | `solve()` polls the API until `status: 'ready'` and returns the `solution` | +| `throws TimeoutError when timeout exceeded` | exceeding `timeout` raises `TimeoutError` | +| `throws ApiError when a task fails during polling` | an error arriving **during polling** turns into `ApiError` | +| `throws ApiError when errorId is not zero` | a non-zero `errorId` in the response → `ApiError` | +| `ApiError carries the string errorCode, not the numeric errorId` | `error.errorCode` holds the string code (`ERROR_KEY_DOES_NOT_EXIST`), not a number | +| `throws NetworkError on fetch failure` | a `fetch` failure is wrapped in `NetworkError` | + +### Tests per captcha type + +For each captcha type the suite checks that the task class serializes into the request body exactly per the API contract, and that `solve()` returns the expected solution shape. + +Common to every type's serialization: +- the `type` field matches the API task type (`RecaptchaV2TaskProxyless`, `TurnstileTask` and so on); +- required fields make it into `toDict()`; +- `null`/`undefined` fields are **stripped** and never reach the request; +- the proxy variants of the classes add `proxyType`/`proxyAddress`/`proxyPort`/`proxyLogin`/`proxyPassword`. + +| File | Tests (sync / async) | What is specific to it | +|---|---|---| +| `recaptcha_v2.test.js` | 6 / 1 | the field names `recaptchaDataSValue` and `apiDomain` (not `dataSValue`); `isInvisible`, `userAgent`; proxy variant. `solve()` goes through an intermediate `status: 'processing'` — exactly 3 requests | +| `recaptcha_v2_enterprise.test.js` | 4 / 1 | `enterprisePayload` as an object, `isInvisible`; an exact `toDict()` match for the minimal field set | +| `recaptcha_v3.test.js` | 4 / 1 | `minScore` is required — without it the constructor throws `ValidationError`; `pageAction`, `apiDomain` | +| `turnstile.test.js` | 4 / 1 | the field names `data` and `pageData` (not `cData`) | +| `geetest.test.js` | 6 / 2 | v3: `gt` + `challenge`, no `version` field. v4: `version: 4` + `initParameters`. Plus `geetestApiServerSubdomain`. `solve()` is tested separately for v3 and v4 — their solution shapes differ | +| `yandex_smartcaptcha.test.js` | 4 / 1 | `userAgent`, `cookies`; proxy variant | +| `tencent.test.js` | 4 / 1 | `appId`, optional `captchaScript` | +| `image_to_text.test.js` | 3 / 1 | the constructor parameter `case_` serializes to the field `case` (working around the reserved word); `numeric`, `phrase`, `minLength`/`maxLength`, `comment`, `imgInstructions` | +| `coordinates.test.js` | 5 / 1 | the `imgType` variants `smart_captcha` (Yandex) and `pazl_smart_captcha`; `imgInstructions`; the `minClicks`/`maxClicks` limits | + +The solution shape expected in `solve()` differs by type: `gRecaptchaResponse` (reCAPTCHA), `token` (Turnstile, Yandex), `text` (ImageToText), `coordinates` (Coordinates), `challenge`/`validate`/`seccode` (GeeTest v3), `captcha_output` and others (GeeTest v4), `ticket`/`randstr` (Tencent). + +### Public API contract + +`tests/unit/public-api.test.js` — 7 tests guarding the `exports` map from [package.json](../package.json): + +```json +"exports": { + ".": "./src/index.js", + "./tasks": "./src/tasks.js", + "./exceptions": "./src/exceptions.js" +} +``` + +Every other test already imports the package by name, so a broken main entry would bring the whole suite down on its own. This file covers what would otherwise go unchecked: + +| Test | What it checks | +|---|---| +| `exposes the client, the task namespace and every error class` | `.` exposes `CaptchaClient`, `Tasks` and all 5 error classes | +| `Tasks namespace exposes every task class` | `Tasks` holds all 16 task classes | +| `__version__ matches the version in package.json` | `__version__` has not drifted from `version` during a release | +| `"captcha-sdk/tasks" exposes every task class` | the `./tasks` subpath resolves and hands back every class | +| `"captcha-sdk/exceptions" exposes every error class` | the `./exceptions` subpath resolves and hands back every class | +| `subpaths and the main entry expose the same classes` | these are the **same** objects, not duplicate module instances — otherwise `instanceof` would break for anyone mixing import styles | +| `internal modules are not reachable as subpaths` | `captcha-sdk/client` is rejected: the file exists, but it is not declared in `exports` and must stay private | + +The class lists in the test are spelled out as explicit arrays rather than derived from the module itself. That is deliberate: comparing an export against itself always passes and checks nothing. Adding a new captcha type means extending the array by hand — and that is exactly the point where the decision to make a class public gets recorded explicitly. + +## Integration tests + +The only set that talks to the real API at `https://api.captcha-solver.com`. One file per check: + +| File | What it checks | Target variables | Test timeout | Spends balance | +|---|---|---|---|---| +| `balance.test.js` | account balance | — | 15 s | no | +| `image_to_text.test.js` | text recognition on an image | — (image ships with the repo) | 130 s | **yes** | +| `coordinates.test.js` | `coordinates` — click points on an image | — (image ships with the repo) | 130 s | **yes** | +| `recaptcha_v2.test.js` | `gRecaptchaResponse` | `RECAPTCHA_V2_URL`, `RECAPTCHA_V2_SITE_KEY` | 130 s | **yes** | +| `recaptcha_v3.test.js` | `gRecaptchaResponse` with `minScore: 0.3` | `RECAPTCHA_V3_URL`, `RECAPTCHA_V3_SITE_KEY` | 190 s | **yes** | +| `turnstile.test.js` | `token` | `TURNSTILE_URL`, `TURNSTILE_SITE_KEY` | 130 s | **yes** | +| `geetest_v4.test.js` | `captcha_output`, `lot_number`, `pass_token` | `GEETEST_V4_URL`, `GEETEST_V4_CAPTCHA_ID` | 310 s | **yes** | +| `yandex_smartcaptcha.test.js` | `token` | `YANDEX_SMARTCAPTCHA_URL`, `YANDEX_SMARTCAPTCHA_SITE_KEY` | 130 s | **yes** | +| `tencent.test.js` | `ticket`, `randstr` | `TENCENT_URL`, `TENCENT_APP_ID` | 130 s | **yes** | + +The split follows **cost, not captcha type**. `balance.test.js` costs nothing and depends on nothing, which makes it a fine smoke test for "the key is alive, the network is up": + +```bash +npm test -- tests/integration/balance.test.js +``` + +Everything else calls `solve()` and takes money off the account on every run. That is why these are run selectively, one file at a time, rather than as a whole set. + +### Why the repository carries no targets + +There is not a single real page URL and not a single widget identifier in the code — **on purpose**. Everything comes from environment variables, with no defaults. `recaptcha_v2.test.js` used to fall back to Google's demo page, and `.env.example` used to carry working keys for reCAPTCHA, Turnstile and GeeTest; that has been removed. + +The identifier differs per vendor: a sitekey for reCAPTCHA, Turnstile and Yandex, an `appId` for Tencent, a `captchaId` for GeeTest v4. Variable names follow the vendor's own term — `TENCENT_APP_ID`, not `TENCENT_SITE_KEY`. + +The practical consequence: **a fresh clone has no working integration check other than the balance one**. That is the price of the decision, not an oversight. Put your own targets in `.env` (it is gitignored); the template with the variable names is [.env.example](../.env.example). + +The exception is the two image-based tests, `image_to_text.test.js` and `coordinates.test.js`. They need neither a page nor a widget identifier, only an image, and the images ship with the repository — [text-captcha.png](../examples/assets/text-captcha.png) and [coordinates-captcha.png](../examples/assets/coordinates-captcha.png). Both work out of the box; a key is all it takes. + +The tests deliberately have no fixtures directory of their own: the very same images are what the `image_to_text.js` and `coordinates.js` examples feed to the API, and one shared copy cannot drift from a second one. The path in the test is resolved through `new URL(..., import.meta.url)`, i.e. relative to the test file itself rather than the working directory. + +In `coordinates.test.js` the image bounds are not hard-coded: width and height are read from the PNG's own IHDR chunk, so the "point lies inside the image" assertion stays correct even if the sample is swapped out. + +The image used to be passed through an `IMAGE_TO_TEXT_BASE64` variable in `.env.example`. The string there was valid, but it encoded a 26×26 pixel image — there was nothing in it to recognise, and the test failed reliably with `ERROR_CAPTCHA_UNSOLVABLE`. The variable is gone. + +### What is not covered here + +- **GeeTest v3.** It needs a fresh session-bound `challenge` scraped from the target page immediately before the task is created. That cannot be driven from static configuration; it would take a scraper, as in [examples/async/geetest_v3.js](../examples/async/geetest_v3.js). +- **Coordinates with `imgInstructions`, and the Yandex mode (`imgType: 'smart_captcha'`).** Both need a second picture — an instruction image — which the repository does not have yet; see the images section in [todo.md](../todo.md). The basic form with `comment` is covered. +- **The proxy variants and reCAPTCHA v2 Enterprise.** They need a working proxy and a site with an Enterprise widget respectively. +- **Cloudflare Challenge pages** in `turnstile.test.js` — only the ordinary widget is covered. Challenge pages need fresh `action`, `data` and `pageData` pulled off the page. + +### The shared helper + +`tests/integration/helpers.js` is not a test: it has no `.test.js` suffix, so the default `testMatch` does not pick it up. It holds what would otherwise be copied into every file: + +| Export | Purpose | +|---|---| +| `apiKey` | `process.env.CAPTCHA_API_KEY` in one place | +| `describeIntegration(name, fn)` | `describe` when a key is present, `describe.skip` when it is not | +| `describeTarget(name, vars, fn)` | the same plus a check on the target variables; passes their values to the callback | +| `createClient(options)` | a client with the key from the environment, built per test; `options` is there for the slow types | + +The file also pulls in `dotenv/config`, so `.env` is read automatically — otherwise a local run would mean exporting a dozen variables by hand. dotenv never overwrites values already present in the environment, so anything passed on the command line or by CI still wins. Unit tests do not import this file and stay free of dotenv. + +### How they switch on + +A suite skips itself when `CAPTCHA_API_KEY` is missing **or** its target variables are unset. The skip happens at the `describe` level, so the output reports it honestly: + +``` +Test Suites: 9 skipped, 0 of 9 total +Tests: 9 skipped, 9 total +``` + +The guard used to sit inside each test (`if (!apiKey) return;`), and a keyless run showed green `passed` for tests that had made no request at all. Skipping at the suite level removes that lie: `skipped` means `skipped`. + +Note that the run still finishes **successfully**, having simply checked nothing. That is not enough for CI, which is why the workflow has a dedicated fail-early step — see [CI / pipeline](#ci--pipeline). + +Optional variables that refine the behaviour: + +| Variable | Purpose | +|---|---| +| `RECAPTCHA_V3_PAGE_ACTION` | the action the site passes to `grecaptcha.execute()` — without a match the site scores the token lower | +| `TENCENT_CAPTCHA_SCRIPT` | for a site that loads the widget from a non-default script URL | +| `IMAGE_TO_TEXT_EXPECTED` | the text on your image; without it the test only asserts that the answer is non-empty | + +## Running them by hand + +Once, before anything else: + +```bash +npm install +``` + +### Requirements + +Node.js 18+ (see `engines` in `package.json`): the SDK relies on the global `fetch`. + +The `--experimental-vm-modules` flag is mandatory — the SDK is pure ESM (`"type": "module"`), and without it Jest cannot load the modules. The npm scripts already pass it; you only need it yourself when invoking Jest directly. The `ExperimentalWarning: VM Modules` line in the output is expected and not an error. + +### All tests + +```bash +npm test +``` + +Runs both the unit and the integration tests (the latter in skip mode when there is no key). + +### Unit tests only + +```bash +npm run test:unit +``` + +Requires nothing: no network, no key. The run takes about a second and a half; the expected result is `21 passed, 81 tests`. + +### Integration tests only + +The most convenient way is to keep a `.env` in the project root (it is gitignored; the template is [.env.example](../.env.example)). The helper pulls in dotenv, so the variables are picked up automatically: + +```bash +cp .env.example .env # fill in the key and the targets you need +npm run test:integration +``` + +The environment works too, and it takes priority over `.env`. + +PowerShell: + +```powershell +$env:CAPTCHA_API_KEY = "your_key" +npm run test:integration +``` + +bash / cmd: + +```bash +CAPTCHA_API_KEY=your_key npm run test:integration +``` + +**Running the whole set at once is usually unnecessary** — that is eight paid tasks per run. Go file by file: + +```bash +# free: key and network +npm test -- tests/integration/balance.test.js + +# one paid check +npm test -- tests/integration/turnstile.test.js +``` + +Suites without their variables configured will skip, so there is no need to fill in `.env` completely — only the types you are checking right now. + +### A single file or a single test + +Arguments after `--` are forwarded to Jest: + +```bash +# one file +npm test -- tests/unit/sync/geetest.test.js + +# every test in one directory +npm test -- tests/unit/async + +# one test by name +npm test -- -t "getBalance returns balance as float" + +# verbose per-test output +npm run test:unit -- --verbose + +# watch mode during development +npm run test:unit -- --watch + +# coverage +npm run test:unit -- --coverage +``` + +All of the above works for integration too — the path just points at a different directory: + +```bash +# one integration file: free, key and network only +npm test -- tests/integration/balance.test.js + +# one integration file: a paid check +npm test -- tests/integration/turnstile.test.js + +# the whole integration directory (same as npm run test:integration) +npm test -- tests/integration + +# one test by name inside a file +npm test -- tests/integration/tencent.test.js -t "solve returns a ticket" + +# verbose output: shows which suites were skipped and are worth a second look +npm run test:integration -- --verbose +``` + +Two caveats specific to integration: + +**A `-t` filter without a path will sweep the integration tests too.** `npm test -- -t "solve"` picks up every paid suite that has its targets configured. Pass the file path alongside `-t` unless you want surprise charges. + +**Jest runs files in parallel**, one worker per file. With few paid files this is harmless, but running the whole directory fires several tasks at the API simultaneously. To make them go one at a time: + +```bash +npm run test:integration -- --runInBand +``` + +Jest can also be invoked directly, but then the flag is yours to pass: + +```bash +node --experimental-vm-modules node_modules/jest/bin/jest.js tests/unit/sync/geetest.test.js +``` + +## Coverage + +```bash +npm run test:unit -- --coverage +``` + +Current state: + +``` +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s +All files | 97.53 | 83.63 | 100 | 97.5 | + client.js | 95.45 | 80.76 | 100 | 95.34 | 35,53 + exceptions.js | 100 | 100 | 100 | 100 | + index.js | 100 | 100 | 100 | 100 | + tasks.js | 100 | 84.52 | 100 | 100 | 26,38,71-77,89,107 +``` + +What is uncovered: the HTTP error branch (`!response.ok`) and the `AbortError` handling in `_request()`, plus some default parameter values in the task constructors. + +The figure counts **unit tests only**. Integration is deliberately left out: without a key those suites skip, and the number would swing depending on whether `CAPTCHA_API_KEY` happened to be available. + +Two settings in [jest.config.js](../jest.config.js) matter specifically for coverage: + +- `collectCoverageFrom: ['src/**/*.js']` — count every file under `src/`, not just the ones imported by tests. Without it a new file that nobody wired up would drop out of the report silently instead of showing 0 %. That is exactly how `src/index.js` used to fall out of the report before the switch to package-name imports, leaving coverage at a comfortable-looking 97.5 % with a completely unchecked entry point. +- `coverageReporters: ['text', 'lcov']` — `text` for the console, `lcov` for the Coveralls upload from CI. + +Artifacts are written to `coverage/` (gitignored). + +## CI / pipeline + +The configuration is [.github/workflows/tests.yml](../.github/workflows/tests.yml). Two jobs: + +| Job | Command | When it runs | Secrets | +|---|---|---|---| +| `unit` | `npm run test:unit -- --coverage` | push to `main`, any pull request, schedule, manual dispatch | none | +| `integration` | `npm run test:integration` | **only** on schedule (daily at 03:00 UTC) and manually via workflow_dispatch | `CAPTCHA_API_KEY` | + +### Why the jobs are split + +- **Unit tests are the mandatory blocking step.** Deterministic, no network, no secrets, a couple of seconds. Safe on any PR, forks included. They run on a Node `18 / 20 / 22` matrix, matching `engines.node: ">=18"`. +- **Integration tests deliberately do not run on pull requests.** They spend real account balance and depend on an external API being up. More importantly, secrets are unavailable in a PR from a fork, so the tests would skip themselves and produce a **false green** instead of an honest "not checked". + +### What the nightly run actually checks + +The `integration` job passes only `CAPTCHA_API_KEY` to the tests. It has no target variables, so among the paid suites everything skips except the two image-based ones — `image_to_text.test.js` and `coordinates.test.js`: they need no targets, and with a live key they **really do hit the API and spend balance**. `recaptcha_v2.test.js` used to work off a hard-coded Google demo page; after targets were dropped from the repository (see [above](#why-the-repository-carries-no-targets)) that is no longer the case. + +So **every night costs two tasks**, one per image. That is the price of the nightly run checking `solve()` at all, rather than the balance alone, which is all it used to check. If that spend is unwanted, the simplest fix is to narrow the step to specific files: `npm test -- tests/integration/balance.test.js`. + +The remaining target-driven solve tests are not planned for CI yet. When they are wanted, the order is: + +1. Add the targets as repository secrets — e.g. `RECAPTCHA_V2_URL` and `RECAPTCHA_V2_SITE_KEY`. +2. Pass them through in the `Run integration tests` step alongside `CAPTCHA_API_KEY`. +3. Run selectively — `npm test -- tests/integration/recaptcha_v2.test.js`, not the whole set, or every night will cost one task per configured type. +4. Extend the fail-early step to check those variables too, otherwise a typo in a secret name turns into a silent skip and a green job. + +### Two details that are easy to miss + +**Coverage is uploaded from one Node version only** (`if: matrix.node-version == 20`). The figure does not depend on the Node version, and three parallel uploads of the same report would only clutter the history in Coveralls. + +**The `integration` job fails loudly when the secret did not come through.** There is a fail-early step before the tests: + +```yaml +- name: Fail early if the API key is missing + run: | + if [ -z "$CAPTCHA_API_KEY" ]; then + echo "::error::secrets.CAPTCHA_API_KEY is not set -- integration tests would silently skip" + exit 1 + fi +``` + +Without it a missing key would look like a successful run: the tests skip themselves and the job goes green having checked nothing. This is precisely the case where a silent skip is more dangerous than a failure. + +### What the pipeline does not have yet + +- A lint step — no linter is configured in the project. +- Publishing to npm. +- Badges in the README — tracked in [todo.md](../todo.md) together with wiring up Coveralls (which needs action in a web UI). diff --git a/tests/integration.test.js b/tests/integration.test.js deleted file mode 100644 index f56ccf6..0000000 --- a/tests/integration.test.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Real-API integration tests. Skipped automatically unless CAPTCHA_API_KEY - * is set in the environment. Run with `npm run test:integration`. - */ - -import { CaptchaClient } from '../src/client.js'; -import * as Tasks from '../src/tasks.js'; - -describe('CaptchaClient integration', () => { - let client; - const apiKey = process.env.CAPTCHA_API_KEY; - - beforeAll(() => { - if (!apiKey) { - console.log('CAPTCHA_API_KEY not set. Skipping integration tests.'); - } - }); - - beforeEach(() => { - if (apiKey) { - client = new CaptchaClient({ clientKey: apiKey }); - } - }); - - test('getBalance returns a number', async () => { - if (!apiKey) return; - const balance = await client.getBalance(); - expect(typeof balance).toBe('number'); - }, 15000); - - test('solve reCAPTCHA v2', async () => { - if (!apiKey) return; - const task = new Tasks.RecaptchaV2Proxyless({ - websiteURL: process.env.RECAPTCHA_V2_URL || 'https://recaptcha-demo.appspot.com/recaptcha-v2-checkbox.php', - websiteKey: process.env.RECAPTCHA_V2_SITE_KEY || '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI' - }); - - const solution = await client.solve(task); - expect(solution.gRecaptchaResponse).toBeDefined(); - }, 120000); -}); diff --git a/tests/integration/balance.test.js b/tests/integration/balance.test.js new file mode 100644 index 0000000..5ee42d1 --- /dev/null +++ b/tests/integration/balance.test.js @@ -0,0 +1,23 @@ +/** + * Integration: account balance against the real API. + * + * The cheap half of the integration suite. getBalance() spends no balance and + * needs no target page, so this doubles as a smoke test that the key is valid + * and the network path to the API works. Run it alone when you want to check + * that much without paying for a solve: + * + * npm test -- tests/integration/balance.test.js + * + * Skipped unless CAPTCHA_API_KEY is set. See helpers.js. + */ + +import { describeIntegration, createClient } from './helpers.js'; + +describeIntegration('getBalance against the real API', () => { + test('returns the balance as a number', async () => { + const balance = await createClient().getBalance(); + + // The API returns the amount as a string; the SDK is expected to coerce it. + expect(typeof balance).toBe('number'); + }, 15000); +}); diff --git a/tests/integration/coordinates.test.js b/tests/integration/coordinates.test.js new file mode 100644 index 0000000..d427e6c --- /dev/null +++ b/tests/integration/coordinates.test.js @@ -0,0 +1,61 @@ +/** + * Integration: solve a click captcha end to end and get back coordinates. + * + * SPENDS REAL BALANCE on every run. + * + * Like image_to_text.test.js, this one needs no target page and no widget + * identifier -- only an image, and the image ships with the repository. Both + * suites read their sample from examples/assets/ rather than from a fixtures + * directory of our own: the same pictures are what the examples feed to the + * API, and one copy cannot drift from the other. Paths are resolved against + * this file, not the working directory, so the suite runs from anywhere. + * + * Only the basic form is covered. `imgInstructions` and the Yandex image mode + * (`imgType: 'smart_captcha'`) both need a second picture -- an instruction + * image -- which the repository does not have yet; see todo.md. + */ + +import fs from 'node:fs'; + +import { Tasks } from 'captcha-sdk'; +import { describeIntegration, createClient } from './helpers.js'; + +const imagePath = new URL('../../examples/assets/coordinates-captcha.png', import.meta.url); + +// The sample is a 4x4 grid captcha whose own header reads "Select all squares +// with street signs". The comment repeats that instruction for the worker, +// because nothing guarantees they read the text baked into the image. +const comment = 'click on all squares with street signs'; + +describeIntegration('Coordinates against the real API', () => { + test('solve returns the clicked points', async () => { + const image = fs.readFileSync(imagePath); + + // The API wants pure base64 with no "data:image/png;base64," prefix. + const task = new Tasks.CoordinatesTask({ body: image.toString('base64'), comment }); + + // Click tasks are image tasks: as fast as image-to-text, so the default + // 120 s client timeout is generous already. + const solution = await createClient().solve(task); + + expect(Array.isArray(solution.coordinates)).toBe(true); + expect(solution.coordinates.length).toBeGreaterThan(0); + + // A point outside the picture is not a valid answer, and asserting that + // catches a whole class of nonsense -- negatives, nulls, coordinates in a + // different unit -- that "is a number" would let through. Dimensions come + // from the PNG's IHDR chunk (bytes 16..24) rather than being hard-coded, + // so swapping the sample cannot silently invalidate the bounds. + const width = image.readUInt32BE(16); + const height = image.readUInt32BE(20); + + for (const point of solution.coordinates) { + expect(typeof point.x).toBe('number'); + expect(typeof point.y).toBe('number'); + expect(point.x).toBeGreaterThanOrEqual(0); + expect(point.y).toBeGreaterThanOrEqual(0); + expect(point.x).toBeLessThanOrEqual(width); + expect(point.y).toBeLessThanOrEqual(height); + } + }, 130000); +}); diff --git a/tests/integration/geetest_v4.test.js b/tests/integration/geetest_v4.test.js new file mode 100644 index 0000000..35752c2 --- /dev/null +++ b/tests/integration/geetest_v4.test.js @@ -0,0 +1,36 @@ +/** + * Integration: solve a real GeeTest v4 challenge end to end. + * + * SPENDS REAL BALANCE on every run. + * + * Needs a target page. Set GEETEST_V4_URL and GEETEST_V4_CAPTCHA_ID, or the + * suite skips. There are deliberately no defaults -- see tests/README.md. + * + * v4 is covered but v3 is not, and that is a deliberate gap: v3 needs a fresh, + * session-specific `challenge` fetched from the target immediately before the + * task is created. A hardcoded one is stale on arrival, so a v3 test would + * have to scrape the target first -- see examples/async/geetest_v3.js for what + * that looks like. v4 identifies the widget by captcha_id, which is stable. + */ + +import { Tasks } from 'captcha-sdk'; +import { describeTarget, createClient } from './helpers.js'; + +describeTarget('GeeTest v4 against the real API', ['GEETEST_V4_URL', 'GEETEST_V4_CAPTCHA_ID'], (env) => { + test('solve returns a captcha_output payload', async () => { + const task = new Tasks.GeeTestProxyless({ + websiteURL: env.GEETEST_V4_URL, + version: 4, + initParameters: { captcha_id: env.GEETEST_V4_CAPTCHA_ID } + }); + + // GeeTest is the slowest supported type, hence the raised client timeout + // and the matching polling interval from the examples. + const solution = await createClient({ timeout: 300000, pollingInterval: 10000 }).solve(task); + + expect(typeof solution.captcha_output).toBe('string'); + expect(solution.captcha_output.length).toBeGreaterThan(0); + expect(solution.lot_number).toBeDefined(); + expect(solution.pass_token).toBeDefined(); + }, 310000); +}); diff --git a/tests/integration/helpers.js b/tests/integration/helpers.js new file mode 100644 index 0000000..6fbda48 --- /dev/null +++ b/tests/integration/helpers.js @@ -0,0 +1,80 @@ +/** + * Shared setup for the real-API integration tests. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../src/. + * + * Note this file is NOT a test file -- it has no .test.js suffix, so Jest's + * default testMatch ignores it. Keep it that way when adding helpers. + */ + +// Loads .env so a local run does not have to export a dozen variables by hand. +// dotenv never overwrites variables already present in the environment, so +// anything passed on the command line or by CI still wins. Missing .env is not +// an error. Unit tests do not import this file and stay unaffected. +import 'dotenv/config'; + +import { CaptchaClient } from 'captcha-sdk'; + +export const apiKey = process.env.CAPTCHA_API_KEY; + +/** + * describe() when a key is available, describe.skip() when it is not. + * + * The suite used to guard every test body with `if (!apiKey) return`, which + * made a keyless run report green "passed" for tests that never touched the + * API -- a silent lie that is worse than a failure. Skipping at the describe + * level reports "skipped" instead, so the output matches reality. + * + * Resolved per call rather than captured at import time, so the module does + * not depend on when Jest installs its globals. + */ +export function describeIntegration(name, fn) { + (apiKey ? describe : describe.skip)(name, fn); +} + +/** + * Same as describeIntegration, but also requires a target to be configured. + * + * Solving a real captcha needs a real page and whatever identifies the widget + * on it -- a sitekey for reCAPTCHA, Turnstile and Yandex, an appId for + * Tencent, a captchaId for GeeTest v4. Those are deliberately absent from this + * repository -- see tests/README.md. Every value comes from the environment, + * so a suite whose variables are unset skips instead of failing on + * `undefined`: + * + * describeTarget('Turnstile', ['TURNSTILE_URL', 'TURNSTILE_SITE_KEY'], (env) => { + * test('...', async () => { + * new Tasks.TurnstileProxyless({ websiteURL: env.TURNSTILE_URL, ... }); + * }); + * }); + * + * The callback receives the collected values. When the suite is skipped they + * are undefined, which is harmless: describe.skip evaluates the block to + * register test names, but never runs the test bodies that read them. + * + * @param {string} name suite name + * @param {string[]} requiredVars environment variables the suite cannot run without + * @param {(env: Record) => void} fn suite body + */ +export function describeTarget(name, requiredVars, fn) { + const env = Object.fromEntries(requiredVars.map((key) => [key, process.env[key]])); + const missing = requiredVars.filter((key) => !env[key]); + const runnable = Boolean(apiKey) && missing.length === 0; + + (runnable ? describe : describe.skip)(name, () => fn(env)); +} + +/** + * A client bound to the environment's key. Built per test rather than shared, + * so nothing carries over between tests. + * + * Pass overrides for slow captcha types, e.g. createClient({ timeout: 300000 }). + * Keep the Jest timeout of such a test above the client timeout, otherwise + * Jest kills the test before the SDK can raise its own TimeoutError and the + * failure says nothing useful. + */ +export function createClient(options = {}) { + return new CaptchaClient({ clientKey: apiKey, ...options }); +} diff --git a/tests/integration/image_to_text.test.js b/tests/integration/image_to_text.test.js new file mode 100644 index 0000000..3eff491 --- /dev/null +++ b/tests/integration/image_to_text.test.js @@ -0,0 +1,48 @@ +/** + * Integration: recognise text on a real captcha image end to end. + * + * SPENDS REAL BALANCE on every run. + * + * The only solve test that needs no target page and no widget identifier -- + * just an image, and it ships with the repository. So unlike every other solve + * suite this one runs on a fresh clone with nothing but CAPTCHA_API_KEY set. + * + * The sample lives in examples/assets/ rather than in a fixtures directory of + * our own: the same picture is what the image_to_text examples feed to the + * API, and one copy that both use cannot drift from the other. Path is + * resolved against this file, not the working directory, so the suite runs + * from anywhere. + * + * IMAGE_TO_TEXT_EXPECTED is optional. Without it the test only asserts that + * some non-empty text came back, which proves the round-trip but not the + * answer. Set it to the text on the image to assert the result itself -- + * compared case-insensitively, since the worker's casing is not guaranteed + * unless the task sets case_. + */ + +import fs from 'node:fs'; + +import { Tasks } from 'captcha-sdk'; +import { describeIntegration, createClient } from './helpers.js'; + +const imagePath = new URL('../../examples/assets/text-captcha.png', import.meta.url); +const expected = process.env.IMAGE_TO_TEXT_EXPECTED; + +describeIntegration('Image to Text against the real API', () => { + test('solve returns the recognised text', async () => { + // The API wants pure base64 with no "data:image/png;base64," prefix. + const body = fs.readFileSync(imagePath).toString('base64'); + const task = new Tasks.ImageToText({ body }); + + // Image tasks are the fastest type, so the default 120 s client timeout + // is generous already. + const solution = await createClient().solve(task); + + expect(typeof solution.text).toBe('string'); + expect(solution.text.length).toBeGreaterThan(0); + + if (expected) { + expect(solution.text.toLowerCase()).toBe(expected.toLowerCase()); + } + }, 130000); +}); diff --git a/tests/integration/recaptcha_v2.test.js b/tests/integration/recaptcha_v2.test.js new file mode 100644 index 0000000..4780f14 --- /dev/null +++ b/tests/integration/recaptcha_v2.test.js @@ -0,0 +1,30 @@ +/** + * Integration: solve a real reCAPTCHA v2 end to end. + * + * SPENDS REAL BALANCE on every run -- it creates an actual task and waits for + * a worker to solve it. This is the suite's proof that the whole path works: + * createTask, polling, and the solution shape the API really returns. + * + * Needs a target page. Set RECAPTCHA_V2_URL and RECAPTCHA_V2_SITE_KEY, or the + * suite skips. There are deliberately no defaults -- see tests/README.md. + * + * Imports the SDK by package name on purpose, not by relative path into src/. + * See tests/README.md for why. Do not "fix" it to ../../src/. + */ + +import { Tasks } from 'captcha-sdk'; +import { describeTarget, createClient } from './helpers.js'; + +describeTarget('reCAPTCHA v2 against the real API', ['RECAPTCHA_V2_URL', 'RECAPTCHA_V2_SITE_KEY'], (env) => { + test('solve returns a gRecaptchaResponse token', async () => { + const task = new Tasks.RecaptchaV2Proxyless({ + websiteURL: env.RECAPTCHA_V2_URL, + websiteKey: env.RECAPTCHA_V2_SITE_KEY + }); + + const solution = await createClient().solve(task); + + expect(typeof solution.gRecaptchaResponse).toBe('string'); + expect(solution.gRecaptchaResponse.length).toBeGreaterThan(0); + }, 130000); +}); diff --git a/tests/integration/recaptcha_v3.test.js b/tests/integration/recaptcha_v3.test.js new file mode 100644 index 0000000..3ad52fb --- /dev/null +++ b/tests/integration/recaptcha_v3.test.js @@ -0,0 +1,37 @@ +/** + * Integration: solve a real reCAPTCHA v3 end to end. + * + * SPENDS REAL BALANCE on every run. + * + * Needs a target page. Set RECAPTCHA_V3_URL and RECAPTCHA_V3_SITE_KEY, or the + * suite skips. There are deliberately no defaults -- see tests/README.md. + * + * RECAPTCHA_V3_PAGE_ACTION is optional: pass it when the target site sets an + * action in grecaptcha.execute(), since a mismatched action lowers the score + * the site assigns to the token. + */ + +import { Tasks } from 'captcha-sdk'; +import { describeTarget, createClient } from './helpers.js'; + +// The cheapest and fastest threshold the API accepts. Not target data -- this +// is a property of the test, so it stays in the file rather than the env. +const MIN_SCORE = 0.3; + +describeTarget('reCAPTCHA v3 against the real API', ['RECAPTCHA_V3_URL', 'RECAPTCHA_V3_SITE_KEY'], (env) => { + test('solve returns a gRecaptchaResponse token', async () => { + const task = new Tasks.RecaptchaV3Proxyless({ + websiteURL: env.RECAPTCHA_V3_URL, + websiteKey: env.RECAPTCHA_V3_SITE_KEY, + minScore: MIN_SCORE, + pageAction: process.env.RECAPTCHA_V3_PAGE_ACTION || null + }); + + // v3 runs longer than v2, so the client gets a longer timeout than the + // 120 s default and the test a longer one still. + const solution = await createClient({ timeout: 180000 }).solve(task); + + expect(typeof solution.gRecaptchaResponse).toBe('string'); + expect(solution.gRecaptchaResponse.length).toBeGreaterThan(0); + }, 190000); +}); diff --git a/tests/integration/tencent.test.js b/tests/integration/tencent.test.js new file mode 100644 index 0000000..2890207 --- /dev/null +++ b/tests/integration/tencent.test.js @@ -0,0 +1,30 @@ +/** + * Integration: solve a real Tencent captcha end to end. + * + * SPENDS REAL BALANCE on every run. + * + * Needs a target page. Set TENCENT_URL and TENCENT_APP_ID, or the suite skips. + * There are deliberately no defaults -- see tests/README.md. + * + * TENCENT_CAPTCHA_SCRIPT is optional: set it only when the target loads the + * widget from a non-default script URL. + */ + +import { Tasks } from 'captcha-sdk'; +import { describeTarget, createClient } from './helpers.js'; + +describeTarget('Tencent against the real API', ['TENCENT_URL', 'TENCENT_APP_ID'], (env) => { + test('solve returns a ticket', async () => { + const task = new Tasks.TencentTaskProxyless({ + websiteURL: env.TENCENT_URL, + appId: env.TENCENT_APP_ID, + captchaScript: process.env.TENCENT_CAPTCHA_SCRIPT || null + }); + + const solution = await createClient().solve(task); + + expect(typeof solution.ticket).toBe('string'); + expect(solution.ticket.length).toBeGreaterThan(0); + expect(solution.randstr).toBeDefined(); + }, 130000); +}); diff --git a/tests/integration/turnstile.test.js b/tests/integration/turnstile.test.js new file mode 100644 index 0000000..b379164 --- /dev/null +++ b/tests/integration/turnstile.test.js @@ -0,0 +1,29 @@ +/** + * Integration: solve a real Cloudflare Turnstile challenge end to end. + * + * SPENDS REAL BALANCE on every run. + * + * Needs a target page. Set TURNSTILE_URL and TURNSTILE_SITE_KEY, or the suite + * skips. There are deliberately no defaults -- see tests/README.md. + * + * Only the plain widget is covered here. Cloudflare Challenge pages also need + * action, data and pageData scraped fresh from the page, which is a different + * kind of test: it would need a scraper of its own to stay meaningful. + */ + +import { Tasks } from 'captcha-sdk'; +import { describeTarget, createClient } from './helpers.js'; + +describeTarget('Turnstile against the real API', ['TURNSTILE_URL', 'TURNSTILE_SITE_KEY'], (env) => { + test('solve returns a token', async () => { + const task = new Tasks.TurnstileProxyless({ + websiteURL: env.TURNSTILE_URL, + websiteKey: env.TURNSTILE_SITE_KEY + }); + + const solution = await createClient().solve(task); + + expect(typeof solution.token).toBe('string'); + expect(solution.token.length).toBeGreaterThan(0); + }, 130000); +}); diff --git a/tests/integration/yandex_smartcaptcha.test.js b/tests/integration/yandex_smartcaptcha.test.js new file mode 100644 index 0000000..462b80f --- /dev/null +++ b/tests/integration/yandex_smartcaptcha.test.js @@ -0,0 +1,33 @@ +/** + * Integration: solve a real Yandex SmartCaptcha (token challenge) end to end. + * + * SPENDS REAL BALANCE on every run. + * + * Needs a target page. Set YANDEX_SMARTCAPTCHA_URL and + * YANDEX_SMARTCAPTCHA_SITE_KEY, or the suite skips. There are deliberately no + * defaults -- see tests/README.md. + * + * This covers the token challenge only. Yandex's image challenge is solved + * through CoordinatesTask with imgType: 'smart_captcha' and needs a captcha + * image plus an instruction image, so it belongs with the image-based tests + * rather than here. + */ + +import { Tasks } from 'captcha-sdk'; +import { describeTarget, createClient } from './helpers.js'; + +const REQUIRED = ['YANDEX_SMARTCAPTCHA_URL', 'YANDEX_SMARTCAPTCHA_SITE_KEY']; + +describeTarget('Yandex SmartCaptcha against the real API', REQUIRED, (env) => { + test('solve returns a token', async () => { + const task = new Tasks.YandexSmartCaptchaTaskProxyless({ + websiteURL: env.YANDEX_SMARTCAPTCHA_URL, + websiteKey: env.YANDEX_SMARTCAPTCHA_SITE_KEY + }); + + const solution = await createClient().solve(task); + + expect(typeof solution.token).toBe('string'); + expect(solution.token.length).toBeGreaterThan(0); + }, 130000); +}); diff --git a/tests/async/client.test.js b/tests/unit/async/client.test.js similarity index 94% rename from tests/async/client.test.js rename to tests/unit/async/client.test.js index 8c55863..5385f46 100644 --- a/tests/async/client.test.js +++ b/tests/unit/async/client.test.js @@ -1,14 +1,16 @@ /** * Generic tests for CaptchaClient (transport, error handling, polling) -- * not tied to any specific captcha type, written in async/await style. - * Mirrors tests/sync/client.test.js. + * Mirrors tests/unit/sync/client.test.js. * See .test.js in this directory for per-type solve() tests. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import { ApiError, NetworkError, TimeoutError, ValidationError } from '../../src/exceptions.js'; -import * as Tasks from '../../src/tasks.js'; +import { ApiError, CaptchaClient, NetworkError, Tasks, TimeoutError, ValidationError } from 'captcha-sdk'; describe('CaptchaClient (async)', () => { let client; diff --git a/tests/async/coordinates.test.js b/tests/unit/async/coordinates.test.js similarity index 63% rename from tests/async/coordinates.test.js rename to tests/unit/async/coordinates.test.js index be3f442..9f9b6bc 100644 --- a/tests/async/coordinates.test.js +++ b/tests/unit/async/coordinates.test.js @@ -1,11 +1,14 @@ /** * Async solve() test for CoordinatesTask. - * Task serialization is covered once in tests/sync/coordinates.test.js. + * Task serialization is covered once in tests/unit/sync/coordinates.test.js. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; test('solve', async () => { const client = new CaptchaClient({ clientKey: 'test_key', pollingInterval: 10 }); diff --git a/tests/async/geetest.test.js b/tests/unit/async/geetest.test.js similarity index 78% rename from tests/async/geetest.test.js rename to tests/unit/async/geetest.test.js index cc9b815..87b10cb 100644 --- a/tests/async/geetest.test.js +++ b/tests/unit/async/geetest.test.js @@ -1,11 +1,14 @@ /** * Async solve() tests for GeeTestProxyless (v3 and v4). - * Task serialization is covered once in tests/sync/geetest.test.js. + * Task serialization is covered once in tests/unit/sync/geetest.test.js. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; test('solve v3', async () => { const client = new CaptchaClient({ clientKey: 'test_key', pollingInterval: 10 }); diff --git a/tests/async/image_to_text.test.js b/tests/unit/async/image_to_text.test.js similarity index 62% rename from tests/async/image_to_text.test.js rename to tests/unit/async/image_to_text.test.js index af33013..2420d96 100644 --- a/tests/async/image_to_text.test.js +++ b/tests/unit/async/image_to_text.test.js @@ -1,11 +1,14 @@ /** * Async solve() test for ImageToText. - * Task serialization is covered once in tests/sync/image_to_text.test.js. + * Task serialization is covered once in tests/unit/sync/image_to_text.test.js. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; test('solve', async () => { const client = new CaptchaClient({ clientKey: 'test_key', pollingInterval: 10 }); diff --git a/tests/async/recaptcha_v2.test.js b/tests/unit/async/recaptcha_v2.test.js similarity index 69% rename from tests/async/recaptcha_v2.test.js rename to tests/unit/async/recaptcha_v2.test.js index 7c17522..5afb0de 100644 --- a/tests/async/recaptcha_v2.test.js +++ b/tests/unit/async/recaptcha_v2.test.js @@ -1,12 +1,15 @@ /** * Async solve() test for RecaptchaV2Proxyless. - * Task serialization is covered once in tests/sync/recaptcha_v2.test.js -- + * Task serialization is covered once in tests/unit/sync/recaptcha_v2.test.js -- * it doesn't depend on which style (sync/async) is used to call solve(). + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; test('solve', async () => { const client = new CaptchaClient({ clientKey: 'test_key', pollingInterval: 10 }); diff --git a/tests/async/recaptcha_v2_enterprise.test.js b/tests/unit/async/recaptcha_v2_enterprise.test.js similarity index 64% rename from tests/async/recaptcha_v2_enterprise.test.js rename to tests/unit/async/recaptcha_v2_enterprise.test.js index 4ab0bd0..51cebf1 100644 --- a/tests/async/recaptcha_v2_enterprise.test.js +++ b/tests/unit/async/recaptcha_v2_enterprise.test.js @@ -1,11 +1,14 @@ /** * Async solve() test for RecaptchaV2EnterpriseProxyless. - * Task serialization is covered once in tests/sync/recaptcha_v2_enterprise.test.js. + * Task serialization is covered once in tests/unit/sync/recaptcha_v2_enterprise.test.js. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; test('solve', async () => { const client = new CaptchaClient({ clientKey: 'test_key', pollingInterval: 10 }); diff --git a/tests/async/recaptcha_v3.test.js b/tests/unit/async/recaptcha_v3.test.js similarity index 64% rename from tests/async/recaptcha_v3.test.js rename to tests/unit/async/recaptcha_v3.test.js index 905fe94..9ba4f65 100644 --- a/tests/async/recaptcha_v3.test.js +++ b/tests/unit/async/recaptcha_v3.test.js @@ -1,11 +1,14 @@ /** * Async solve() test for RecaptchaV3Proxyless. - * Task serialization is covered once in tests/sync/recaptcha_v3.test.js. + * Task serialization is covered once in tests/unit/sync/recaptcha_v3.test.js. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; test('solve', async () => { const client = new CaptchaClient({ clientKey: 'test_key', pollingInterval: 10 }); diff --git a/tests/async/tencent.test.js b/tests/unit/async/tencent.test.js similarity index 66% rename from tests/async/tencent.test.js rename to tests/unit/async/tencent.test.js index e2a565a..84f6f70 100644 --- a/tests/async/tencent.test.js +++ b/tests/unit/async/tencent.test.js @@ -1,11 +1,14 @@ /** * Async solve() test for TencentTaskProxyless. - * Task serialization is covered once in tests/sync/tencent.test.js. + * Task serialization is covered once in tests/unit/sync/tencent.test.js. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; test('solve', async () => { const client = new CaptchaClient({ clientKey: 'test_key', pollingInterval: 10 }); diff --git a/tests/async/turnstile.test.js b/tests/unit/async/turnstile.test.js similarity index 63% rename from tests/async/turnstile.test.js rename to tests/unit/async/turnstile.test.js index f41e7c9..1f39d21 100644 --- a/tests/async/turnstile.test.js +++ b/tests/unit/async/turnstile.test.js @@ -1,11 +1,14 @@ /** * Async solve() test for TurnstileProxyless. - * Task serialization is covered once in tests/sync/turnstile.test.js. + * Task serialization is covered once in tests/unit/sync/turnstile.test.js. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; test('solve', async () => { const client = new CaptchaClient({ clientKey: 'test_key', pollingInterval: 10 }); diff --git a/tests/async/yandex_smartcaptcha.test.js b/tests/unit/async/yandex_smartcaptcha.test.js similarity index 63% rename from tests/async/yandex_smartcaptcha.test.js rename to tests/unit/async/yandex_smartcaptcha.test.js index 9418288..43020c1 100644 --- a/tests/async/yandex_smartcaptcha.test.js +++ b/tests/unit/async/yandex_smartcaptcha.test.js @@ -1,11 +1,14 @@ /** * Async solve() test for YandexSmartCaptchaTaskProxyless. - * Task serialization is covered once in tests/sync/yandex_smartcaptcha.test.js. + * Task serialization is covered once in tests/unit/sync/yandex_smartcaptcha.test.js. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; test('solve', async () => { const client = new CaptchaClient({ clientKey: 'test_key', pollingInterval: 10 }); diff --git a/tests/unit/public-api.test.js b/tests/unit/public-api.test.js new file mode 100644 index 0000000..045029d --- /dev/null +++ b/tests/unit/public-api.test.js @@ -0,0 +1,98 @@ +/** + * Contract tests for the package's public surface. + * + * Every other test file imports the SDK by package name, so a broken main + * entry point would already fail the suite loudly. What those tests do NOT + * cover is the rest of the "exports" map in package.json: the ./tasks and + * ./exceptions subpaths, and the fact that undeclared subpaths stay private. + * That is what this file guards. + */ + +import fs from 'node:fs'; + +import * as pkg from 'captcha-sdk'; +import * as tasksEntry from 'captcha-sdk/tasks'; +import * as exceptionsEntry from 'captcha-sdk/exceptions'; + +const TASK_CLASSES = [ + 'BaseTask', + 'RecaptchaV2Proxyless', + 'RecaptchaV2', + 'RecaptchaV2EnterpriseProxyless', + 'RecaptchaV2Enterprise', + 'RecaptchaV3Proxyless', + 'TurnstileProxyless', + 'Turnstile', + 'GeeTestProxyless', + 'GeeTest', + 'ImageToText', + 'YandexSmartCaptchaTaskProxyless', + 'YandexSmartCaptchaTask', + 'CoordinatesTask', + 'TencentTaskProxyless', + 'TencentTask' +]; + +const ERROR_CLASSES = [ + 'CaptchaError', + 'ApiError', + 'NetworkError', + 'TimeoutError', + 'ValidationError' +]; + +describe('main entry point ("captcha-sdk")', () => { + test('exposes the client, the task namespace and every error class', () => { + expect(typeof pkg.CaptchaClient).toBe('function'); + expect(typeof pkg.Tasks).toBe('object'); + + for (const name of ERROR_CLASSES) { + expect(typeof pkg[name]).toBe('function'); + } + }); + + test('Tasks namespace exposes every task class', () => { + for (const name of TASK_CLASSES) { + expect(typeof pkg.Tasks[name]).toBe('function'); + } + }); + + test('__version__ matches the version in package.json', () => { + const { version } = JSON.parse( + fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8') + ); + + expect(pkg.__version__).toBe(version); + }); +}); + +describe('subpath entry points', () => { + test('"captcha-sdk/tasks" exposes every task class', () => { + for (const name of TASK_CLASSES) { + expect(typeof tasksEntry[name]).toBe('function'); + } + }); + + test('"captcha-sdk/exceptions" exposes every error class', () => { + for (const name of ERROR_CLASSES) { + expect(typeof exceptionsEntry[name]).toBe('function'); + } + }); + + test('subpaths and the main entry expose the same classes', () => { + for (const name of TASK_CLASSES) { + expect(tasksEntry[name]).toBe(pkg.Tasks[name]); + } + for (const name of ERROR_CLASSES) { + expect(exceptionsEntry[name]).toBe(pkg[name]); + } + }); +}); + +describe('module privacy', () => { + test('internal modules are not reachable as subpaths', async () => { + // Only ".", "./tasks" and "./exceptions" are declared in "exports"; + // ./client must stay private even though the file exists. + await expect(import('captcha-sdk/client')).rejects.toThrow(); + }); +}); diff --git a/tests/sync/client.test.js b/tests/unit/sync/client.test.js similarity index 94% rename from tests/sync/client.test.js rename to tests/unit/sync/client.test.js index 5a68bec..818a945 100644 --- a/tests/sync/client.test.js +++ b/tests/unit/sync/client.test.js @@ -1,15 +1,17 @@ /** * Generic tests for CaptchaClient (transport, error handling, polling) -- * not tied to any specific captcha type, written in promise-chain style - * (.then/.catch, no async/await). Mirrors tests/async/client.test.js. + * (.then/.catch, no async/await). Mirrors tests/unit/async/client.test.js. * See .test.js in this directory for per-type task * serialization tests. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import { ApiError, NetworkError, TimeoutError, ValidationError } from '../../src/exceptions.js'; -import * as Tasks from '../../src/tasks.js'; +import { ApiError, CaptchaClient, NetworkError, Tasks, TimeoutError, ValidationError } from 'captcha-sdk'; describe('CaptchaClient (sync-style)', () => { let client; diff --git a/tests/sync/coordinates.test.js b/tests/unit/sync/coordinates.test.js similarity index 88% rename from tests/sync/coordinates.test.js rename to tests/unit/sync/coordinates.test.js index 10faaf0..76db8d4 100644 --- a/tests/sync/coordinates.test.js +++ b/tests/unit/sync/coordinates.test.js @@ -1,11 +1,14 @@ /** * Tests for CoordinatesTask task serialization (generic click captcha + * Yandex SmartCaptcha image mode), plus a solve() test in promise-chain style. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; describe('CoordinatesTask', () => { test('toDict includes body and comment', () => { diff --git a/tests/sync/geetest.test.js b/tests/unit/sync/geetest.test.js similarity index 91% rename from tests/sync/geetest.test.js rename to tests/unit/sync/geetest.test.js index e86877e..6977c5e 100644 --- a/tests/sync/geetest.test.js +++ b/tests/unit/sync/geetest.test.js @@ -1,11 +1,14 @@ /** * Tests for GeeTestProxyless / GeeTest task serialization (v3 and v4), * plus solve() tests in promise-chain style. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; describe('GeeTest v3 and v4', () => { test('toDict for v3 excludes version field', () => { diff --git a/tests/sync/image_to_text.test.js b/tests/unit/sync/image_to_text.test.js similarity index 86% rename from tests/sync/image_to_text.test.js rename to tests/unit/sync/image_to_text.test.js index 3d14f53..d06036b 100644 --- a/tests/sync/image_to_text.test.js +++ b/tests/unit/sync/image_to_text.test.js @@ -1,11 +1,14 @@ /** * Tests for ImageToText task serialization, plus a solve() test in * promise-chain style. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; describe('ImageToText', () => { test('toDict includes basic fields', () => { diff --git a/tests/sync/recaptcha_v2.test.js b/tests/unit/sync/recaptcha_v2.test.js similarity index 92% rename from tests/sync/recaptcha_v2.test.js rename to tests/unit/sync/recaptcha_v2.test.js index f659bf8..8d944ed 100644 --- a/tests/sync/recaptcha_v2.test.js +++ b/tests/unit/sync/recaptcha_v2.test.js @@ -2,11 +2,14 @@ * Tests for RecaptchaV2Proxyless / RecaptchaV2 task serialization, plus a * solve() test in promise-chain style. Serialization is covered once here -- * it doesn't depend on which style (sync/async) is used to call solve(). + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; describe('RecaptchaV2Proxyless', () => { test('toDict includes required fields', () => { diff --git a/tests/sync/recaptcha_v2_enterprise.test.js b/tests/unit/sync/recaptcha_v2_enterprise.test.js similarity index 89% rename from tests/sync/recaptcha_v2_enterprise.test.js rename to tests/unit/sync/recaptcha_v2_enterprise.test.js index 09752f6..304113b 100644 --- a/tests/sync/recaptcha_v2_enterprise.test.js +++ b/tests/unit/sync/recaptcha_v2_enterprise.test.js @@ -1,11 +1,14 @@ /** * Tests for RecaptchaV2EnterpriseProxyless / RecaptchaV2Enterprise task * serialization, plus a solve() test in promise-chain style. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; describe('RecaptchaV2EnterpriseProxyless', () => { test('toDict includes required fields', () => { diff --git a/tests/sync/recaptcha_v3.test.js b/tests/unit/sync/recaptcha_v3.test.js similarity index 84% rename from tests/sync/recaptcha_v3.test.js rename to tests/unit/sync/recaptcha_v3.test.js index c80db48..a5e05d9 100644 --- a/tests/sync/recaptcha_v3.test.js +++ b/tests/unit/sync/recaptcha_v3.test.js @@ -1,12 +1,14 @@ /** * Tests for RecaptchaV3Proxyless task serialization, plus a solve() test * in promise-chain style. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import { ValidationError } from '../../src/exceptions.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks, ValidationError } from 'captcha-sdk'; describe('RecaptchaV3Proxyless', () => { test('requires minScore', () => { diff --git a/tests/sync/tencent.test.js b/tests/unit/sync/tencent.test.js similarity index 88% rename from tests/sync/tencent.test.js rename to tests/unit/sync/tencent.test.js index 9e1a4e8..a2fa661 100644 --- a/tests/sync/tencent.test.js +++ b/tests/unit/sync/tencent.test.js @@ -1,11 +1,14 @@ /** * Tests for TencentTaskProxyless / TencentTask task serialization, plus a * solve() test in promise-chain style. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; describe('TencentTaskProxyless', () => { test('toDict includes required fields', () => { diff --git a/tests/sync/turnstile.test.js b/tests/unit/sync/turnstile.test.js similarity index 87% rename from tests/sync/turnstile.test.js rename to tests/unit/sync/turnstile.test.js index f3667bc..191adcf 100644 --- a/tests/sync/turnstile.test.js +++ b/tests/unit/sync/turnstile.test.js @@ -1,11 +1,14 @@ /** * Tests for TurnstileProxyless / Turnstile task serialization, plus a * solve() test in promise-chain style. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; describe('TurnstileProxyless', () => { test('toDict includes required fields', () => { diff --git a/tests/sync/yandex_smartcaptcha.test.js b/tests/unit/sync/yandex_smartcaptcha.test.js similarity index 88% rename from tests/sync/yandex_smartcaptcha.test.js rename to tests/unit/sync/yandex_smartcaptcha.test.js index 5bfca21..8a89350 100644 --- a/tests/sync/yandex_smartcaptcha.test.js +++ b/tests/unit/sync/yandex_smartcaptcha.test.js @@ -1,11 +1,14 @@ /** * Tests for YandexSmartCaptchaTaskProxyless / YandexSmartCaptchaTask task * serialization, plus a solve() test in promise-chain style. + * + * Imports the SDK by package name on purpose, not by relative path into + * src/: that is what makes the suite exercise the package's real entry + * points. See tests/README.md for why. Do not "fix" it to ../../../src/. */ import { jest } from '@jest/globals'; -import { CaptchaClient } from '../../src/client.js'; -import * as Tasks from '../../src/tasks.js'; +import { CaptchaClient, Tasks } from 'captcha-sdk'; describe('YandexSmartCaptchaTaskProxyless', () => { test('toDict includes required fields', () => { diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..916278b --- /dev/null +++ b/todo.md @@ -0,0 +1,106 @@ +# TODO + +## Бейдж покрытия тестами в README + +**Статус:** отложено. Вся техническая подготовка сделана — осталось только то, что требует действий в веб-интерфейсе. + +### Что уже готово + +- [x] `jest.config.js` с `collectCoverageFrom: ['src/**/*.js']` — покрытие считается по всем файлам `src/`, а не только по импортированным из тестов. +- [x] Тесты импортируют SDK по имени пакета (`captcha-sdk`), а не по прямым путям к файлам — покрытие отражает реальный публичный API. +- [x] `tests/unit/public-api.test.js` — проверка контракта всех трёх точек входа из `exports`. +- [x] CI-workflow `.github/workflows/tests.yml` — прогоняет unit-тесты и выгружает `coverage/lcov.info` в Coveralls. + +### Что осталось сделать + +- [ ] **Подключить репозиторий к Coveralls.** Зайти на [coveralls.io](https://coveralls.io), авторизоваться через GitHub, включить репозиторий `captcha-solver-api/javascript-sdk` в списке. Отдельный секрет не нужен — GitHub Action использует встроенный `GITHUB_TOKEN`. +- [ ] **Дождаться первого прогона CI на `main`.** До него бейдж будет отдавать `unknown`. +- [ ] **Добавить бейджи в начало `README.md`** (сразу под заголовком, перед описанием): + + ```markdown + [![Tests](https://github.com/captcha-solver-api/javascript-sdk/actions/workflows/tests.yml/badge.svg)](https://github.com/captcha-solver-api/javascript-sdk/actions/workflows/tests.yml) + [![Coverage Status](https://coveralls.io/repos/github/captcha-solver-api/javascript-sdk/badge.svg?branch=main)](https://coveralls.io/github/captcha-solver-api/javascript-sdk?branch=main) + ``` + +### Чем заблокировано + +**Репозиторий сейчас приватный, а бесплатный план Coveralls работает только с публичными.** Приватные не показываются в списке — при попытке подключить сервис отвечает `No repos found`, а выгрузка из CI падает с `Couldn't find a repository matching this job`. + +Ждём, когда репозиторий станет публичным. После этого настройка заработает как есть, переписывать ничего не нужно. + +До тех пор шаг выгрузки в workflow тихо отваливается и джоб не роняет — за это отвечает `fail-on-error: false`. + +Если решение сделать репозиторий публичным изменится, вариантов два: платный план Coveralls либо генерация бейджа локально через `istanbul-badges-readme` (без внешнего сервиса; чтобы бейдж не устаревал, в CI регенерировать его и падать по `git diff --exit-code README.md`). + +### Почему Coveralls, а не Codecov + +Для GitHub Actions Coveralls не требует отдельного секрета — хватает встроенного `GITHUB_TOKEN`. Codecov с 2024 года требует токен даже для публичных репозиториев, а такой секрет всё равно не будет доступен в PR из форков — то есть для внешних контрибьюторов выгрузка покрытия будет молча ломаться. + +### На что обратить внимание + +Покрытие считается **только по unit-тестам** (`npm run test:unit`). Integration-тесты в подсчёт не входят намеренно: без `CAPTCHA_API_KEY` они пропускаются, и цифра покрытия скакала бы в зависимости от того, был ли доступен ключ. + +--- + +## Изображения для примеров с картинками + +**Статус:** основное сделано. Все четыре примера запускаются из коробки — нужен только ключ. Осталось добавить две картинки-инструкции и раскомментировать три блока, которые их ждут. + +### В чём была проблема + +`image_to_text.js` и `coordinates.js` (в обоих каталогах, `async/` и `sync/`) начинали работу с чтения картинки, которой в репозитории не было, и падали с `ENOENT` на первой же строке — ещё до обращения к API. Хуже того, путь был передан как `./captcha.png` и резолвился **относительно рабочего каталога** (`process.cwd()`), а не относительно файла скрипта: положить картинки рядом со скриптами было бы недостаточно, `node examples/async/coordinates.js` из корня репозитория всё равно искал бы их в корне. + +Что читается сейчас: + +| Файл | Основная картинка | Картинка-инструкция (`imgInstructions`) | +|---|---|---| +| `examples/*/image_to_text.js` | `../assets/text-captcha.png` — **есть** | `../assets/text-captcha-hint.png` — нет, блок *Advanced* закомментирован | +| `examples/*/coordinates.js` | `../assets/coordinates-captcha.png` — **есть** | `../assets/coordinates-captcha-instruction.png` — нет, блоки *Advanced* и *Yandex* закомментированы | + +### Что сделать + +- [x] **Завести `examples/assets/`** — рядом с тем, что картинки использует. Корневой `assets/` занят баннером репозитория, мешать одно с другим не стоит. Имена — kebab-case, как у `assets/repo-banner-javascript.png`, и по типу капчи, а не по имени примера. +- [x] **Текстовая капча** — `examples/assets/text-captcha.png`. Её же читает `tests/integration/image_to_text.test.js`: одна копия на примеры и тесты, дублировать бинарник в `tests/` не нужно. +- [x] **Кликовая капча** — `examples/assets/coordinates-captcha.png`, её читает `tests/integration/coordinates.test.js`. +- [x] **Поправить пути в четырёх файлах**, чтобы они не зависели от рабочего каталога: + + ```javascript + const body = fs.readFileSync(new URL('../assets/text-captcha.png', import.meta.url)).toString('base64'); + ``` + + `new URL(..., import.meta.url)` работает начиная с Node 14 и подходит под `engines.node: ">=18"`. Вариант с `import.meta.dirname` требует Node 20.11+ и планку по Node поднимет. +- [x] **Подсказки в `image_to_text.js` подогнаны под картинку.** Было `numeric: 1` (только цифры) при буквенной капче — воркеру уходила заведомо ложная подсказка. Стало `numeric: 2` (буквы), в обоих блоках, где эти поля есть. +- [x] **`comment` в `coordinates.js`** — `'click on all squares with street signs'` вместо `'click on the green apple'`: повторяет инструкцию, напечатанную на самой картинке. +- [x] **Обновлены [examples/README.md](examples/README.md), [examples/sync/README.md](examples/sync/README.md), [examples/async/README.md](examples/async/README.md).** Раздел «Before you run», описания обоих примеров и оговорки про рабочий каталог. +- [ ] **Добавить две картинки-инструкции:** `text-captcha-hint.png` (к текстовой) и `coordinates-captcha-instruction.png` (к кликовой) — и раскомментировать три блока, которые их ждут: *Advanced* в `image_to_text.js`, *Advanced* и *Yandex SmartCaptcha image mode* в `coordinates.js`. Тогда же можно будет покрыть режим `imgType: 'smart_captcha'` в `tests/integration/coordinates.test.js`. +- [ ] **Заодно решить, что делать с блоком *Advanced* в `image_to_text.js`.** Он подписан как математическая капча (`math: true`), а `text-captcha.png` — буквенная. Либо третья картинка с примером-уравнением, либо переписать блок под ту же картинку. + +### Отвергнутая альтернатива: base64 в `.env.example` + +Раньше картинка для теста передавалась строкой `IMAGE_TO_TEXT_BASE64` из `.env.example`, и примеры могли бы брать `body` оттуда же — без бинарников в репозитории. Не годится по двум причинам. + +Во-первых, лежавшая там строка была валидным PNG, но размером 26×26 пикселей: распознавать в ней нечего, и `image_to_text.test.js` стабильно падал с `ERROR_CAPTCHA_UNSOLVABLE`, тратя баланс. Переменная убрана вместе со строкой. + +Во-вторых, для `coordinates.js` это не работает в принципе: там нужны две картинки, и пришлось бы добавлять ещё две длинные base64-строки, что читаемости файлу не добавит. + +### На что обратить внимание + +`examples/` и `assets/` целиком исключены из публикуемого пакета в [.npmignore](.npmignore), а `files: ["src/"]` в `package.json` и так работает как белый список. Так что вес картинок на размер npm-тарбола не повлияет — только на размер клона репозитория. + +--- + +## Идеи на будущее (не приоритет) + +- [ ] `coverageThreshold` в `jest.config.js` — чтобы CI падал при просадке покрытия ниже порога. Имеет смысл включать после того, как бейдж заработает и станет понятен реальный базовый уровень. +- [ ] Линтер (ESLint) — сейчас в проекте не настроен, в CI отдельного шага линтинга нет. +- [ ] Тест на содержимое публикуемого пакета (`npm pack`) — проверить, что в тарбол попадает всё нужное из `files: ["src/"]`. +- [ ] agent.md + +--- +Добавление метаданных по идентификации библиотек и примеров + +--- +нет подсказок для методов решения капч и других методов +--- +а почему нету option примеров ? +--- \ No newline at end of file