├── .github ├── funding.yml └── workflows │ └── test.yml ├── .npmrc ├── .prettierignore ├── lib ├── index.js ├── utils.js ├── check-links.js └── check-link.js ├── .editorconfig ├── .eslintrc.json ├── .prettierrc ├── .gitignore ├── tsconfig.json ├── license ├── package.json ├── test ├── check-link.test.js └── check-links.test.js └── readme.md /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [transitive-bullshit] 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | package-manager-strict=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .snapshots/ 2 | .next/ 3 | .vercel/ 4 | build/ 5 | dist/ 6 | docs/ 7 | *.yaml 8 | *.d.ts 9 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export * from './utils.js' 2 | export * from './check-link.js' 3 | export * from './check-links.js' 4 | 5 | export { checkLinks as default } from './check-links.js' 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@fisch0920/eslint-config/node"], 4 | "rules": { 5 | "@typescript-eslint/naming-convention": "off", 6 | "unicorn/no-for-loop": "off", 7 | "array-callback-return": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "bracketSpacing": true, 8 | "bracketSameLine": false, 9 | "arrowParens": "always", 10 | "trailingComma": "none" 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # builds 7 | build 8 | dist 9 | 10 | # misc 11 | .DS_Store 12 | .env 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | .cache 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | *.d.ts 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["lib/**/*.js"], 3 | "compilerOptions": { 4 | "lib": ["esnext", "dom.iterable"], 5 | "module": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "target": "es2020", 9 | "outDir": "dist", 10 | 11 | "allowImportingTsExtensions": false, 12 | "allowJs": true, 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "incremental": false, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "noUncheckedIndexedAccess": true, 19 | "resolveJsonModule": true, 20 | "skipLibCheck": true, 21 | "sourceMap": true, 22 | "strict": true, 23 | "useDefineForClassFields": true, 24 | "verbatimModuleSyntax": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: true 11 | matrix: 12 | node-version: 13 | - 18 14 | - 22 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v3 22 | id: pnpm-install 23 | with: 24 | version: 9.12.3 25 | run_install: false 26 | 27 | - name: Install Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | cache: 'pnpm' 32 | 33 | - name: Install dependencies 34 | run: pnpm install --frozen-lockfile --strict-peer-dependencies 35 | 36 | - name: Run test 37 | run: pnpm test 38 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | import isRelativeUrl from 'is-relative-url' 2 | 3 | export const protocolWhitelist = new Set(['https:', 'http:']) 4 | 5 | export const userAgent = 6 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' 7 | 8 | /** 9 | * @typedef {object} Options 10 | * @property {string} [baseUrl] 11 | * @property {unknown} [agent] 12 | * @property {import('http').IncomingHttpHeaders} [headers] 13 | * @property {unknown} [timeout] 14 | */ 15 | 16 | /** 17 | * 18 | * @param {string} url 19 | * @param {Options} [opts] 20 | * @returns 21 | */ 22 | export function isValidUrl(url, opts) { 23 | if (isRelativeUrl(url)) { 24 | return opts && !!opts.baseUrl 25 | } else { 26 | try { 27 | const parsedUrl = new URL(url) 28 | return protocolWhitelist.has(parsedUrl.protocol) 29 | } catch { 30 | // invalid URL 31 | return false 32 | } 33 | } 34 | } 35 | 36 | // exporting utils in this way allows us to stub them in tests using `sinon` 37 | export const utils = { 38 | isValidUrl, 39 | userAgent 40 | } 41 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Travis Fischer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "check-links", 3 | "version": "3.0.1", 4 | "description": "Robustly checks an array of URLs for liveness.", 5 | "repository": "transitive-bullshit/check-links", 6 | "author": "Travis Fischer ", 7 | "license": "MIT", 8 | "type": "module", 9 | "module": "./dist/index.js", 10 | "types": "./dist/index.d.ts", 11 | "exports": { 12 | "types": "./dist/index.d.ts", 13 | "default": "./dist/index.js" 14 | }, 15 | "sideEffects": false, 16 | "files": [ 17 | "dist" 18 | ], 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "scripts": { 23 | "build": "tsc", 24 | "docs": "update-markdown-jsdoc --no-markdown-toc --shallow", 25 | "prebuild": "rimraf dist", 26 | "pretest": "run-s build", 27 | "test": "run-s test:*", 28 | "test:format": "prettier --check \"**/*.{js,ts,tsx}\"", 29 | "test:lint": "eslint .", 30 | "test:typecheck": "tsc --noEmit", 31 | "test:unit": "vitest run" 32 | }, 33 | "keywords": [ 34 | "url", 35 | "liveness", 36 | "alive", 37 | "dead", 38 | "404", 39 | "500", 40 | "200", 41 | "check", 42 | "link", 43 | "link-check", 44 | "url-check" 45 | ], 46 | "dependencies": { 47 | "expiry-map": "^2.0.0", 48 | "got": "^14.4.4", 49 | "is-relative-url": "^4.0.0", 50 | "p-map": "^7.0.2", 51 | "p-memoize": "^7.1.1" 52 | }, 53 | "devDependencies": { 54 | "@fisch0920/eslint-config": "^1.4.0", 55 | "@types/sinon": "^17.0.3", 56 | "nock": "^13.5.5", 57 | "npm-run-all2": "^7.0.1", 58 | "prettier": "^3.3.3", 59 | "rimraf": "^6.0.1", 60 | "sinon": "^19.0.2", 61 | "typescript": "^5.6.3", 62 | "vitest": "2.1.4" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/check-links.js: -------------------------------------------------------------------------------- 1 | import pMap from 'p-map' 2 | import pMemoize from 'p-memoize' 3 | import ExpiryMap from 'expiry-map' 4 | 5 | import { checkLink } from './check-link.js' 6 | 7 | /** @type {import('expiry-map')} */ 8 | const cache = new ExpiryMap(60 * 1000) 9 | const isUrlAlive = pMemoize(checkLink, { cache }) 10 | 11 | /** 12 | * @typedef {{[url: string]: import('./check-link.js').LivenessResult}} LivenessResultMap 13 | */ 14 | 15 | /** 16 | * Robustly checks an array of URLs for liveness. 17 | * 18 | * For each URL, it first attempts an HTTP HEAD request, and if that fails it will attempt 19 | * an HTTP GET request, retrying several times by default with exponential falloff. 20 | * 21 | * Returns a `Map` that maps each input URL to an object 22 | * containing `status` and possibly `statusCode`. 23 | * 24 | * `LivenessResult.status` will be one of the following: 25 | * - `alive` if the URL is reachable (2XX status code) 26 | * - `dead` if the URL is not reachable 27 | * - `invalid` if the URL was parsed as invalid or used an unsupported protocol 28 | * 29 | * `LivenessResult.statusCode` will contain an integer HTTP status code if that URL resolved 30 | * properly. 31 | * 32 | * @name checkLinks 33 | * @function 34 | * 35 | * @param {Array} urls - Array of urls to test 36 | * @param {Omit & Pick} [opts] - Optional configuration options (any extra options are passed to [got](https://github.com/sindresorhus/got#options)) 37 | * 38 | * @return {Promise} 39 | */ 40 | export async function checkLinks(urls, opts = {}) { 41 | const concurrency = opts.concurrency || 8 42 | /** @type {LivenessResultMap} */ 43 | const results = {} 44 | 45 | await pMap( 46 | urls, 47 | async (url) => { 48 | const result = await isUrlAlive(url, opts) 49 | results[url] = result 50 | }, 51 | { 52 | concurrency 53 | } 54 | ) 55 | 56 | return results 57 | } 58 | -------------------------------------------------------------------------------- /test/check-link.test.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import sinon from 'sinon' 3 | 4 | import { checkLink, utils } from '../lib/index.js' 5 | 6 | test.sequential('check-links default options', async () => { 7 | const stub = sinon.stub(utils, 'isValidUrl').callsFake((url, opts) => { 8 | // @ts-expect-error options will always be passed here 9 | const { agent, ...rest } = opts 10 | 11 | expect(url).toBe('invalid') 12 | 13 | expect(rest).toEqual({ 14 | headers: { 15 | 'user-agent': utils.userAgent, 16 | 'Upgrade-Insecure-Requests': '1', 17 | connection: 'keep-alive', 18 | accept: 19 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 20 | 'accept-encoding': 'gzip, deflate, br', 21 | 'cache-control': 'max-age=0', 22 | 'accept-language': 'en-US,en;q=0.9' 23 | }, 24 | timeout: { request: 30_000 } 25 | }) 26 | 27 | return true 28 | }) 29 | 30 | await checkLink('invalid') 31 | 32 | stub.restore() 33 | }) 34 | 35 | test.sequential('check-links overriding got options', async () => { 36 | const stub = sinon.stub(utils, 'isValidUrl').callsFake((_url, opts) => { 37 | // @ts-expect-error options will always be passed here 38 | const { agent, ...rest } = opts 39 | 40 | expect(rest).toEqual({ 41 | headers: { 42 | 'user-agent': utils.userAgent, 43 | 'Upgrade-Insecure-Requests': '1', 44 | connection: 'keep-alive', 45 | accept: 46 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 47 | 'accept-encoding': 'gzip, deflate, br', 48 | 'cache-control': 'max-age=0', 49 | 'accept-language': 'en-US,en;q=0.9', 50 | authorization: 'test' 51 | }, 52 | timeout: { request: 10_000 }, 53 | retry: { limit: 5 } 54 | }) 55 | 56 | return true 57 | }) 58 | 59 | await checkLink('invalid', { 60 | headers: { 61 | authorization: 'test' 62 | }, 63 | timeout: { request: 10_000 }, 64 | retry: { limit: 5 } 65 | }) 66 | 67 | stub.restore() 68 | }) 69 | -------------------------------------------------------------------------------- /lib/check-link.js: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | import http from 'node:http' 3 | import https from 'node:https' 4 | import { utils } from './utils.js' 5 | 6 | export const agent = { 7 | http: new http.Agent(), 8 | https: new https.Agent({ rejectUnauthorized: false }) 9 | } 10 | 11 | /** 12 | * @typedef {import('got').HTTPError} HTTPError 13 | * @typedef {import('got').Response} Response 14 | * 15 | * @typedef {object} LivenessResult 16 | * @property {'invalid' | 'alive' | 'dead'} status 17 | * @property {number} [statusCode] 18 | */ 19 | 20 | /** 21 | * Checks if a URL is alive (2XX status code). 22 | * 23 | * @name checkLink 24 | * @function 25 | * 26 | * @param {string} url - URL to test 27 | * @param {import('got').OptionsOfTextResponseBody} [opts] - Optional configuration options passed to got 28 | * 29 | * @return {Promise} 30 | */ 31 | export function checkLink(url, opts = {}) { 32 | const { headers = {}, ...rest } = opts 33 | 34 | opts = { 35 | headers: { 36 | 'user-agent': utils.userAgent, 37 | 'Upgrade-Insecure-Requests': '1', 38 | connection: 'keep-alive', 39 | accept: 40 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 41 | 'accept-encoding': 'gzip, deflate, br', 42 | 'cache-control': 'max-age=0', 43 | 'accept-language': 'en-US,en;q=0.9', 44 | ...headers 45 | }, 46 | agent, 47 | timeout: { 48 | request: 30_000 49 | }, 50 | ...rest 51 | } 52 | 53 | if (!utils.isValidUrl(url, opts)) { 54 | return Promise.resolve({ 55 | status: 'invalid' 56 | }) 57 | } 58 | 59 | // We only allow retrying the GET request because we don't want to wait on 60 | // exponential falloff for failed HEAD request and failed GET requests. 61 | const fetchHEAD = () => 62 | got.head(url, { ...opts, retry: { limit: 0 } }).then( 63 | (res) => 64 | /** @type {const} */ ({ 65 | status: 'alive', 66 | statusCode: res.statusCode 67 | }) 68 | ) 69 | 70 | const fetchGET = () => 71 | got(url, opts).then( 72 | (res) => 73 | /** @type {const} */ ({ 74 | status: 'alive', 75 | statusCode: res.statusCode 76 | }) 77 | ) 78 | 79 | return fetchHEAD() 80 | .catch((/** @type {HTTPError} */ err) => { 81 | // TODO: if HEAD results in a `got.HTTPError`, are there status codes where 82 | // we can bypass the GET request? 83 | return fetchGET() 84 | }) 85 | .catch((/** @type {Response} */ err) => { 86 | return /** @type {const} */ ({ 87 | status: 'dead', 88 | statusCode: err.statusCode ?? err.response?.statusCode 89 | }) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # check-links 2 | 3 | > Robustly checks an array of URLs for liveness. 4 | 5 | [![NPM](https://img.shields.io/npm/v/check-links.svg)](https://www.npmjs.com/package/check-links) [![Build Status](https://github.com/transitive-bullshit/check-links/actions/workflows/test.yml/badge.svg)](https://github.com/transitive-bullshit/check-links/actions/workflows/test.yml) [![Prettier Code Formatting](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 6 | 7 | For each URL, it first attempts an HTTP HEAD request, and if that fails it will attempt 8 | an HTTP GET request, retrying several times by default with exponential falloff. 9 | 10 | This module handles concurrency and retry logic so you can check the status of thousands 11 | of links quickly and robustly. 12 | 13 | ## Install 14 | 15 | This module requires `node >= 18`. 16 | 17 | ```bash 18 | npm install --save check-links 19 | # or 20 | yarn add check-links 21 | # or 22 | pnpm add check-links 23 | ``` 24 | 25 | Note: this package uses ESM and no longer provides a CommonJS export. See [here](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) for more info on how to use ESM modules. 26 | 27 | ## Usage 28 | 29 | ```js 30 | import checkLinks from 'check-links' 31 | 32 | const results = await checkLinks(['https://foo.com', 'https://404.com']) 33 | 34 | results['https://foo.com'] // { status: 'alive', statusCode: 200 } 35 | results['https://404.com'] // { status: 'dead', statusCode: 404 } 36 | 37 | // example using a custom concurrency, timeout, and retry count 38 | const results2 = await checkLinks(['https://foo.com', 'https://404.com'], { 39 | concurrency: 1, 40 | timeout: { request: 30000 }, 41 | retry: { limit: 1 } 42 | }) 43 | ``` 44 | 45 | - Supports HTTP and HTTPS urls. 46 | - Defaults to a 30 second timeout per HTTP request with 2 retries. 47 | - Defaults to a Mac OS Chrome `user-agent`. 48 | - Defaults to following redirects. 49 | - All options use a [got options object](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md). 50 | 51 | ## API 52 | 53 | 54 | 55 | ### [checkLinks](https://github.com/transitive-bullshit/check-links/blob/cf5fbcf0fc0bd150097034887b7c132384794549/index.js#L34-L51) 56 | 57 | Robustly checks an array of URLs for liveness. 58 | 59 | For each URL, it first attempts an HTTP HEAD request, and if that fails it will attempt 60 | an HTTP GET request, retrying several times by default with exponential falloff. 61 | 62 | Returns a `Map` that maps each input URL to an object 63 | containing `status` and possibly `statusCode`. 64 | 65 | `LivenessResult.status` will be one of the following: 66 | 67 | - `alive` if the URL is reachable (2XX status code) 68 | - `dead` if the URL is not reachable 69 | - `invalid` if the URL was parsed as invalid or used an unsupported protocol 70 | 71 | `LivenessResult.statusCode` will contain an integer HTTP status code if that URL resolved 72 | properly. 73 | 74 | Type: `function (urls, opts)` 75 | 76 | - `urls` **[array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** Array of urls to test 77 | - `opts` **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)?** Optional configuration options (any extra options are passed to [got](https://github.com/sindresorhus/got#options)) 78 | - `opts.concurrency` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** Maximum number of urls to resolve concurrently (optional, default `8`) 79 | 80 | ## Related 81 | 82 | - [remark-lint-no-dead-urls](https://github.com/davidtheclark/remark-lint-no-dead-urls) - Remark lint plugin that inspired this module. 83 | 84 | ## License 85 | 86 | MIT © [Travis Fischer](https://github.com/transitive-bullshit) 87 | 88 | Support my OSS work by following me on twitter twitter 89 | -------------------------------------------------------------------------------- /test/check-links.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, beforeAll } from 'vitest' 2 | import nock from 'nock' 3 | 4 | import checkLinks from '../lib/index.js' 5 | 6 | const aliveUrls = ['https://123.com/', 'http://456.com/', 'https://789.net/'] 7 | 8 | const aliveGETUrls = [ 9 | 'https://get-foo.com/', 10 | 'https://get-bar.com/', 11 | 'https://get-baz.net/' 12 | ] 13 | 14 | const deadUrls = [ 15 | 'https://a.net/', 16 | 'https://b.net/', 17 | 'https://c.net/', 18 | 'https://d.neti/', 19 | 'https://e.net/', 20 | 'https://f.net/', 21 | 'https://g.net/', 22 | 'https://h.net/', 23 | 'https://i.net/' 24 | ] 25 | 26 | const invalidUrls = ['ftp://123.com', 'mailto:foo@bar.com', 'foobar'] 27 | 28 | const allUrls = aliveUrls 29 | .concat(aliveGETUrls) 30 | .concat(deadUrls) 31 | .concat(invalidUrls) 32 | 33 | beforeAll(() => { 34 | for (const url of aliveUrls) { 35 | nock(url).persist().intercept('/', 'HEAD').reply(200) 36 | } 37 | 38 | for (const url of aliveGETUrls) { 39 | nock(url) 40 | .persist() 41 | .intercept('/', 'HEAD') 42 | .reply(405) 43 | .intercept('/', 'GET') 44 | .reply(200) 45 | } 46 | 47 | for (const url of deadUrls) { 48 | nock(url) 49 | .persist() 50 | .intercept('/', 'HEAD') 51 | .reply(400) 52 | .intercept('/', 'GET') 53 | .reply(400) 54 | .intercept('/404', 'HEAD') 55 | .reply(404) 56 | .intercept('/404', 'GET') 57 | .reply(404) 58 | .intercept('/500', 'HEAD') 59 | .reply(500) 60 | .intercept('/500', 'GET') 61 | .reply(500) 62 | } 63 | }) 64 | 65 | test('check-links alive urls HEAD', async () => { 66 | const results = await checkLinks(aliveUrls) 67 | expect(Object.keys(results).length).toBe(aliveUrls.length) 68 | 69 | for (const url in results) { 70 | expect(results[url]).toEqual({ 71 | status: 'alive', 72 | statusCode: 200 73 | }) 74 | } 75 | }) 76 | 77 | test('check-links alive urls GET', async () => { 78 | const results = await checkLinks(aliveGETUrls) 79 | expect(Object.keys(results).length).toBe(aliveGETUrls.length) 80 | 81 | for (const url in results) { 82 | expect(results[url]).toEqual({ 83 | status: 'alive', 84 | statusCode: 200 85 | }) 86 | } 87 | }) 88 | 89 | test('check-links invalid urls', async () => { 90 | const results = await checkLinks(invalidUrls) 91 | expect(Object.keys(results).length).toBe(invalidUrls.length) 92 | 93 | for (const url in results) { 94 | expect(results[url]).toEqual({ 95 | status: 'invalid' 96 | }) 97 | } 98 | }) 99 | 100 | test( 101 | 'check-links dead urls 500', 102 | { 103 | timeout: 30_000 104 | }, 105 | async () => { 106 | const results = await checkLinks(deadUrls.map((url) => `${url}500`)) 107 | expect(Object.keys(results).length).toBe(deadUrls.length) 108 | 109 | for (const url in results) { 110 | expect(results[url]).toEqual({ 111 | status: 'dead', 112 | statusCode: 500 113 | }) 114 | } 115 | } 116 | ) 117 | 118 | test( 119 | 'check-links dead urls 404', 120 | { 121 | timeout: 30_000 122 | }, 123 | async () => { 124 | const results = await checkLinks(deadUrls.map((url) => `${url}404`)) 125 | expect(Object.keys(results).length).toBe(deadUrls.length) 126 | 127 | for (const url in results) { 128 | expect(results[url]).toEqual({ 129 | status: 'dead', 130 | statusCode: 404 131 | }) 132 | } 133 | } 134 | ) 135 | 136 | test('check-links mixed alive / dead / invalid urls', async () => { 137 | const results = await checkLinks(allUrls) 138 | expect(Object.keys(results).length).toBe(allUrls.length) 139 | 140 | for (const url of aliveUrls) { 141 | expect(results[url]).toEqual({ 142 | status: 'alive', 143 | statusCode: 200 144 | }) 145 | } 146 | 147 | for (const url of aliveGETUrls) { 148 | expect(results[url]).toEqual({ 149 | status: 'alive', 150 | statusCode: 200 151 | }) 152 | } 153 | 154 | for (const url of deadUrls) { 155 | expect(results[url]).toEqual({ 156 | status: 'dead', 157 | statusCode: 400 158 | }) 159 | } 160 | 161 | for (const url of invalidUrls) { 162 | expect(results[url]).toEqual({ 163 | status: 'invalid' 164 | }) 165 | } 166 | }) 167 | --------------------------------------------------------------------------------