From 6f799a8db42778b0b811f6130dd4fec4b7c63671 Mon Sep 17 00:00:00 2001 From: Szymon Marczak <36894700+szmarczak@users.noreply.github.com> Date: Sat, 9 May 2020 19:23:44 +0200 Subject: [PATCH] Rewrite main logic (#31) --- README.md | 90 ++++---- benchmark.js | 2 + package.json | 3 +- source/hosts-resolver.js | 161 --------------- source/index.js | 253 ++++++++++++++++------- tests/crlfHosts.txt | 17 -- tests/hosts.txt | 17 -- tests/test.js | 435 +++++++++++++++++++-------------------- tests/travisHosts.txt | 5 - tests/watching.txt | 0 10 files changed, 436 insertions(+), 547 deletions(-) delete mode 100644 source/hosts-resolver.js delete mode 100644 tests/crlfHosts.txt delete mode 100644 tests/hosts.txt delete mode 100644 tests/travisHosts.txt delete mode 100644 tests/watching.txt diff --git a/README.md b/README.md index e29f830..2507155 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Making lots of HTTP requests? You can save some time by caching DNS lookups :zap ```js const http = require('http'); const CacheableLookup = require('cacheable-lookup'); + const cacheable = new CacheableLookup(); http.get('http://example.com', {lookup: cacheable.lookup}, response => { @@ -28,8 +29,8 @@ http.get('http://example.com', {lookup: cacheable.lookup}, response => { ```js const http = require('http'); const CacheableLookup = require('cacheable-lookup'); -const cacheable = new CacheableLookup(); +const cacheable = new CacheableLookup(); cacheable.install(http.globalAgent); http.get('http://example.com', response => { @@ -43,7 +44,14 @@ http.get('http://example.com', response => { Returns a new instance of `CacheableLookup`. -#### cache +#### options + +Type: `object`
+Default: `{}` + +Options used to cache the DNS lookups. + +##### cache Type: `Map` | [`Keyv`](https://github.com/lukechilds/keyv/)
Default: `new Map()` @@ -52,12 +60,21 @@ Custom cache instance. If `undefined`, it will create a new one. **Note**: If you decide to use Keyv instead of the native implementation, the performance will drop by 10x. Memory leaks may occur as it doesn't provide any way to remove all the deprecated values at once. -#### options +**Tip**: [`QuickLRU`](https://github.com/sindresorhus/quick-lru) is fully compatible with the Map API, you can use it to limit the amount of cached entries. Example: -Type: `object`
-Default: `{}` +```js +const http = require('http'); +const CacheableLookup = require('cacheable-lookup'); +const QuickLRU = require('quick-lru'); -Options used to cache the DNS lookups. +const cacheable = new CacheableLookup({ + cache: new QuickLRU({maxSize: 1000}) +}); + +http.get('http://example.com', {lookup: cacheable.lookup}, response => { + // Handle the response here +}); +``` ##### options.maxTtl @@ -70,23 +87,21 @@ If set to `0`, it will make a new DNS query each time. **Pro Tip**: This shouldn't be lower than your DNS server response time in order to prevent bottlenecks. For example, if you use Cloudflare, this value should be greater than `0.01`. -##### options.fallbackTtl +##### options.fallbackDuration Type: `number`
-Default: `1` - -The lifetime of the entries received from the OS (TTL in seconds). +Default: `3600` (1 hour) -**Note**: This option is independent, `options.maxTtl` does not affect this. +When the DNS server responds with `ENOTFOUND` or `ENODATA` and the OS reports that the entry is available, it will use `dns.lookup(...)` directly for the requested hostnames for the specified amount of time (in seconds). -**Pro Tip**: This shouldn't be lower than your DNS server response time in order to prevent bottlenecks. For example, if you use Cloudflare, this value should be greater than `0.01`. +If you don't query internal hostnames (such as `localhost`, `database.local` etc.), it is strongly recommended to set this value to `0`. ##### options.errorTtl Type: `number`
Default: `0.15` -The time how long it needs to remember queries that threw `ENOTFOUND` (TTL in seconds). +The time how long it needs to remember queries that threw `ENOTFOUND` or `ENODATA` (TTL in seconds). **Note**: This option is independent, `options.maxTtl` does not affect this. @@ -99,19 +114,14 @@ Default: [`new dns.promises.Resolver()`](https://nodejs.org/api/dns.html#dns_cla An instance of [DNS Resolver](https://nodejs.org/api/dns.html#dns_class_dns_resolver) used to make DNS queries. -##### options.customHostsPath +##### options.lookup -Type: `string`
-Default: `undefined` (OS-specific) +Type: `Function`
+Default: [`dns.lookup`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) -The full path to the `hosts` file. Set this to `false` to prevent loading entries from the `hosts` file. +The fallback function to use when the DNS server responds with `ENOTFOUND` or `ENODATA`. -##### options.watchingHostsFile - -Type: `boolean`
-Default: `false` - -If set to `true`, it will watch the `hosts` file and update the cache. +**Note**: This has no effect if the `fallbackDuration` option is less than `1`. ### Entry object @@ -133,13 +143,13 @@ The IP family (`4` or `6`). Type: `number` -**Note**: This is not present when using the native `dns.lookup(...)`! +**Note**: This is not present when falling back to `dns.lookup(...)`! The timestamp (`Date.now() + ttl * 1000`) when the entry expires. #### ttl -**Note**: This is not present when using the native `dns.lookup(...)`! +**Note**: This is not present when falling back to `dns.lookup(...)`! The time in seconds for its lifetime. @@ -154,7 +164,7 @@ When `options.all` is `true`, then `callback(error, entries)` is called. Type: `Array` -The DNS servers used to make queries. Can be overridden - doing so will trigger `cacheableLookup.updateInterfaceInfo()`. +The DNS servers used to make queries. Can be overridden - doing so will clear the cache. #### [lookup(hostname, options, callback)](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) @@ -165,8 +175,6 @@ The asynchronous version of `dns.lookup(…)`. Returns an [entry object](#entry-object).
If `options.all` is true, returns an array of entry objects. -**Note**: If entry(ies) were not found, it will return `undefined` by default. - ##### hostname Type: `string` @@ -193,19 +201,15 @@ This is used by `query(hostname)` if no entry in the database is present. Returns an array of objects with `address`, `family`, `ttl` and `expires` properties. -#### tick() - -Deprecated - it is a noop. Outdated entries are removed automatically. - #### updateInterfaceInfo() Updates interface info. For example, you need to run this when you plug or unplug your WiFi driver. -**Note:** Running `updateInterfaceInfo()` will also trigger `clear()`! +**Note:** Running `updateInterfaceInfo()` will trigger `clear()` only on network interface removal. #### clear(hostname?) -Clears the cache for the given hostname. If the hostname argument is not present, the entire cache will be cleared. +Clears the cache for the given hostname. If the hostname argument is not present, the entire cache will be emptied. ## High performance @@ -215,24 +219,18 @@ Performed on: - CPU governor: performance ``` -CacheableLookup#lookupAsync x 2,421,707 ops/sec ±1.11% (86 runs sampled) -CacheableLookup#lookupAsync.all x 2,338,741 ops/sec ±1.74% (84 runs sampled) -CacheableLookup#lookupAsync.all.ADDRCONFIG x 2,238,534 ops/sec ±0.94% (89 runs sampled) -CacheableLookup#lookup x 2,298,645 ops/sec ±1.26% (87 runs sampled) -CacheableLookup#lookup.all x 2,260,194 ops/sec ±1.49% (87 runs sampled) -CacheableLookup#lookup.all.ADDRCONFIG x 2,133,142 ops/sec ±1.52% (86 runs sampled) +CacheableLookup#lookupAsync x 2,896,251 ops/sec ±1.07% (85 runs sampled) +CacheableLookup#lookupAsync.all x 2,842,664 ops/sec ±1.11% (88 runs sampled) +CacheableLookup#lookupAsync.all.ADDRCONFIG x 2,598,283 ops/sec ±1.21% (88 runs sampled) +CacheableLookup#lookup x 2,565,913 ops/sec ±1.56% (85 runs sampled) +CacheableLookup#lookup.all x 2,609,039 ops/sec ±1.01% (86 runs sampled) +CacheableLookup#lookup.all.ADDRCONFIG x 2,416,242 ops/sec ±0.89% (85 runs sampled) dns#lookup x 7,272 ops/sec ±0.36% (86 runs sampled) dns#lookup.all x 7,249 ops/sec ±0.40% (86 runs sampled) dns#lookup.all.ADDRCONFIG x 5,693 ops/sec ±0.28% (85 runs sampled) Fastest is CacheableLookup#lookupAsync.all ``` -The package is based on [`dns.resolve4(…)`](https://nodejs.org/api/dns.html#dns_dns_resolve4_hostname_options_callback) and [`dns.resolve6(…)`](https://nodejs.org/api/dns.html#dns_dns_resolve6_hostname_options_callback). - -[Why not `dns.lookup(…)`?](https://github.com/nodejs/node/issues/25560#issuecomment-455596215) - -> It is not possible to use `dns.lookup(…)` because underlying calls like [getaddrinfo](http://man7.org/linux/man-pages/man3/getaddrinfo.3.html) have no concept of servers or TTL (caching is done on OS level instead). - ## Related - [cacheable-request](https://github.com/lukechilds/cacheable-request) - Wrap native HTTP requests with RFC compliant cache support diff --git a/benchmark.js b/benchmark.js index 464b3b7..67a9193 100644 --- a/benchmark.js +++ b/benchmark.js @@ -51,5 +51,7 @@ suite.add('CacheableLookup#lookupAsync', deferred => { (async () => { await cacheable.lookupAsync(query); + await new Promise(resolve => setTimeout(resolve, 150)); + suite.run(); })(); diff --git a/package.json b/package.json index fbe7c82..332aaa8 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,14 @@ "homepage": "https://github.com/szmarczak/cacheable-lookup#readme", "devDependencies": { "@types/keyv": "^3.1.1", - "ava": "^3.7.1", + "ava": "^3.8.2", "benchmark": "^2.1.4", "coveralls": "^3.0.9", "keyv": "^4.0.0", "nyc": "^15.0.0", "proxyquire": "^2.1.3", "tsd": "^0.11.0", + "quick-lru": "^5.1.0", "xo": "^0.25.3" } } diff --git a/source/hosts-resolver.js b/source/hosts-resolver.js deleted file mode 100644 index 58813e4..0000000 --- a/source/hosts-resolver.js +++ /dev/null @@ -1,161 +0,0 @@ -'use strict'; -const path = require('path'); -const {watch} = require('fs'); -const {readFile} = require('fs').promises; -const {isIP} = require('net'); - -const isWindows = process.platform === 'win32'; -const hostsPath = isWindows ? path.join(process.env.SystemDrive, 'Windows\\System32\\drivers\\etc\\hosts') : '/etc/hosts'; - -const hostnameRegExp = /^(?:(?:[a-zA-Z\d]|[a-zA-Z\d][a-zA-Z\d-]*[a-zA-Z\d])\.)*(?:[A-Za-z\d]|[A-Za-z\d][A-Za-z\d-]*[A-Za-z\d])$/; -const isHostname = hostname => hostnameRegExp.test(hostname); - -const fileOptions = { - encoding: 'utf8' -}; - -const whitespaceRegExp = /\s+/g; - -class HostsResolver { - constructor({watching, customHostsPath = hostsPath}) { - this._hostsPath = customHostsPath; - this._error = null; - this._watcher = null; - this._watching = watching; - this._hosts = {}; - - this._init(); - } - - _init() { - if (typeof this._hostsPath !== 'string') { - return; - } - - this._promise = (async () => { - await this._update(); - - this._promise = null; - - if (this._error) { - return; - } - - if (this._watching) { - this._watcher = watch(this._hostsPath, { - persistent: false - }, eventType => { - if (eventType === 'change') { - this._update(); - } else { - this._watcher.close(); - } - }); - - this._watcher.once('error', error => { - this._error = error; - this._hosts = {}; - }); - - this._watcher.once('close', () => { - this._init(); - }); - } - })(); - } - - async _update() { - try { - let lines = await readFile(this._hostsPath, fileOptions); - lines = lines.split('\n'); - - this._hosts = {}; - - for (let line of lines) { - line = line.replace(whitespaceRegExp, ' ').trim(); - - const parts = line.split(' '); - - const family = isIP(parts[0]); - if (!family) { - continue; - } - - const address = parts.shift(); - - for (const hostname of parts) { - if (!isHostname(hostname)) { - break; - } - - if (this._hosts[hostname]) { - let shouldAbort = false; - - for (const entry of this._hosts[hostname]) { - if (entry.family === family) { - shouldAbort = true; - break; - } - } - - if (shouldAbort) { - continue; - } - } else { - this._hosts[hostname] = []; - this._hosts[hostname].expires = Infinity; - } - - this._hosts[hostname].push({ - address, - family, - expires: Infinity, - ttl: Infinity - }); - } - } - } catch (error) { - this._hosts = {}; - this._error = error; - } - } - - async get(hostname) { - if (this._promise) { - await this._promise; - } - - if (this._error) { - throw this._error; - } - - return this._hosts[hostname]; - } -} - -const resolvers = {}; - -const getResolver = ({customHostsPath, watching}) => { - if (customHostsPath !== undefined && typeof customHostsPath !== 'string') { - customHostsPath = false; - } - - watching = Boolean(watching); - - const id = `${customHostsPath}:${watching}`; - - let resolver = resolvers[id]; - - if (resolver) { - return resolver; - } - - resolver = new HostsResolver({customHostsPath, watching}); - resolvers[id] = resolver; - - return resolver; -}; - -HostsResolver.getResolver = getResolver; - -module.exports = HostsResolver; diff --git a/source/index.js b/source/index.js index a53b8b3..2186330 100644 --- a/source/index.js +++ b/source/index.js @@ -2,17 +2,20 @@ const { V4MAPPED, ADDRCONFIG, + ALL, promises: { Resolver: AsyncResolver }, - lookup + lookup: dnsLookup } = require('dns'); const {promisify} = require('util'); const os = require('os'); -const {getResolver: getHostsResolver} = require('./hosts-resolver'); const kCacheableLookupCreateConnection = Symbol('cacheableLookupCreateConnection'); const kCacheableLookupInstance = Symbol('cacheableLookupInstance'); +const kExpires = Symbol('expires'); + +const supportsALL = typeof ALL === 'number'; const verifyAgent = agent => { if (!(agent && typeof agent.createConnection === 'function')) { @@ -22,6 +25,10 @@ const verifyAgent = agent => { const map4to6 = entries => { for (const entry of entries) { + if (entry.family === 6) { + continue; + } + entry.address = `::ffff:${entry.address}`; entry.family = 6; } @@ -52,26 +59,28 @@ const getIfaceInfo = () => { return {has4, has6}; }; +const isIterable = map => { + return Symbol.iterator in map; +}; + const ttl = {ttl: true}; +const all = {all: true}; class CacheableLookup { constructor({ - customHostsPath, - watchingHostsFile = false, cache = new Map(), maxTtl = Infinity, + fallbackDuration = 3600, + errorTtl = 0.15, resolver = new AsyncResolver(), - fallbackTtl = 1, - errorTtl = 0.15 + lookup = dnsLookup } = {}) { this.maxTtl = maxTtl; - this.fallbackTtl = fallbackTtl; this.errorTtl = errorTtl; this._cache = cache; this._resolver = resolver; - - this._lookup = promisify(lookup); + this._dnsLookup = promisify(lookup); if (this._resolver instanceof AsyncResolver) { this._resolve4 = this._resolver.resolve4.bind(this._resolver); @@ -82,18 +91,32 @@ class CacheableLookup { } this._iface = getIfaceInfo(); - this._hostsResolver = getHostsResolver({customHostsPath, watching: watchingHostsFile}); this._pending = {}; - this._nextRemovalTime = false; + this._hostnamesToFallback = new Set(); + + if (fallbackDuration < 1) { + this._fallback = false; + } else { + this._fallback = true; + + const interval = setInterval(() => { + this._hostnamesToFallback.clear(); + }, fallbackDuration * 1000); + + /* istanbul ignore next: There is no `interval.unref()` when running inside an Electron renderer */ + if (interval.unref) { + interval.unref(); + } + } this.lookup = this.lookup.bind(this); this.lookupAsync = this.lookupAsync.bind(this); } set servers(servers) { - this.updateInterfaceInfo(); + this.clear(); this._resolver.setServers(servers); } @@ -138,8 +161,12 @@ class CacheableLookup { if (options.family === 6) { const filtered = cached.filter(entry => entry.family === 6); - if (filtered.length === 0 && options.hints & V4MAPPED) { - map4to6(cached); + if (options.hints & V4MAPPED) { + if ((supportsALL && options.hints & ALL) || filtered.length === 0) { + map4to6(cached); + } else { + cached = filtered; + } } else { cached = filtered; } @@ -153,7 +180,7 @@ class CacheableLookup { } if (cached.length === 0) { - const error = new Error(`ENOTFOUND ${hostname}`); + const error = new Error(`cacheableLookup ENOTFOUND ${hostname}`); error.code = 'ENOTFOUND'; error.hostname = hostname; @@ -164,15 +191,11 @@ class CacheableLookup { return cached; } - if (cached.length === 1) { - return cached[0]; - } - - return this._getEntry(cached, hostname); + return cached[0]; } async query(hostname) { - let cached = await this._hostsResolver.get(hostname) || await this._cache.get(hostname); + let cached = await this._cache.get(hostname); if (!cached) { const pending = this._pending[hostname]; @@ -194,89 +217,156 @@ class CacheableLookup { return cached; } - async queryAndCache(hostname) { - // We could make an ANY query, but DNS servers may reject that. - const [As, AAAAs] = await Promise.all([this._resolve4(hostname, ttl).catch(() => []), this._resolve6(hostname, ttl).catch(() => [])]); + async _resolve(hostname) { + const wrap = async promise => { + try { + return await promise; + } catch (error) { + if (error.code === 'ENODATA' || error.code === 'ENOTFOUND') { + return []; + } + throw error; + } + }; + + // ANY is unsafe as it doesn't trigger new queries in the underlying server. + const [A, AAAA] = await Promise.all([ + this._resolve4(hostname, ttl), + this._resolve6(hostname, ttl) + ].map(promise => wrap(promise))); + + let aTtl = 0; + let aaaaTtl = 0; let cacheTtl = 0; - if (As) { - for (const entry of As) { - entry.family = 4; - entry.expires = Date.now() + (entry.ttl * 1000); + const now = Date.now(); - // Is the TTL the same for all entries? - cacheTtl = Math.max(cacheTtl, entry.ttl); - } + for (const entry of A) { + entry.family = 4; + entry.expires = now + (entry.ttl * 1000); + + aTtl = Math.max(aTtl, entry.ttl); } - if (AAAAs) { - for (const entry of AAAAs) { - entry.family = 6; - entry.expires = Date.now() + (entry.ttl * 1000); + for (const entry of AAAA) { + entry.family = 6; + entry.expires = now + (entry.ttl * 1000); - // Is the TTL the same for all entries? - cacheTtl = Math.max(cacheTtl, entry.ttl); + aaaaTtl = Math.max(aaaaTtl, entry.ttl); + } + + if (A.length > 0) { + if (AAAA.length > 0) { + cacheTtl = Math.min(aTtl, aaaaTtl); + } else { + cacheTtl = aTtl; } + } else { + cacheTtl = aaaaTtl; } - let entries = [...(As || []), ...(AAAAs || [])]; + return { + entries: [ + ...A, + ...AAAA + ], + cacheTtl, + isLookup: false + }; + } - if (entries.length === 0) { - try { - entries = await this._lookup(hostname, { - all: true - }); + async _lookup(hostname) { + const empty = { + entries: [], + cacheTtl: 0, + isLookup: true + }; - for (const entry of entries) { - entry.ttl = this.fallbackTtl; - entry.expires = Date.now() + (entry.ttl * 1000); - } + if (!this._fallback) { + return empty; + } - cacheTtl = this.fallbackTtl * 1000; - } catch (error) { - delete this._pending[hostname]; + try { + const entries = await this._dnsLookup(hostname, { + all: true + }); - if (error.code === 'ENOTFOUND') { - cacheTtl = this.errorTtl * 1000; + return { + entries, + cacheTtl: 0, + isLookup: true + }; + } catch (_) { + return empty; + } + } - entries.expires = Date.now() + cacheTtl; - await this._cache.set(hostname, entries, cacheTtl); + async _set(hostname, data, cacheTtl) { + if (this.maxTtl > 0 && cacheTtl > 0) { + cacheTtl = Math.min(cacheTtl, this.maxTtl) * 1000; + data[kExpires] = Date.now() + cacheTtl; - this._tick(cacheTtl); - } + try { + await this._cache.set(hostname, data, cacheTtl); + } catch (error) { + this.lookupAsync = async () => { + const cacheError = new Error('Cache Error. Please recreate the CacheableLookup instance.'); + cacheError.cause = error; - throw error; + throw cacheError; + }; + } + + if (isIterable(this._cache)) { + this._tick(cacheTtl); } - } else { - cacheTtl = Math.min(this.maxTtl, cacheTtl) * 1000; } + } - if (this.maxTtl > 0 && cacheTtl > 0) { - entries.expires = Date.now() + cacheTtl; - await this._cache.set(hostname, entries, cacheTtl); + async queryAndCache(hostname) { + if (this._hostnamesToFallback.has(hostname)) { + return this._dnsLookup(hostname, all); + } + + const resolverPromise = this._resolve(hostname); + const lookupPromise = this._lookup(hostname); + + let query = await Promise.race([ + resolverPromise, + lookupPromise + ]); - this._tick(cacheTtl); + if (query.isLookup && query.entries.length === 0) { + query = await resolverPromise; } - delete this._pending[hostname]; + (async () => { + if (query.isLookup) { + try { + const realDnsQuery = await resolverPromise; + + // If no DNS entries found + if (realDnsQuery.entries.length === 0) { + // Use `dns.lookup(...)` for that particular hostname + this._hostnamesToFallback.add(hostname); + } else { + await this._set(hostname, realDnsQuery.entries, realDnsQuery.cacheTtl); + } + } catch (_) {} + } else { + const cacheTtl = query.entries.length === 0 ? this.errorTtl : query.cacheTtl; - return entries; - } + await this._set(hostname, query.entries, cacheTtl); + } - // eslint-disable-next-line no-unused-vars - _getEntry(entries, hostname) { - return entries[0]; - } + delete this._pending[hostname]; + })(); - /* istanbul ignore next: deprecated */ - tick() {} + return query.entries; + } _tick(ms) { - if (!(this._cache instanceof Map) || ms === undefined) { - return; - } - const nextRemovalTime = this._nextRemovalTime; if (!nextRemovalTime || ms < nextRemovalTime) { @@ -291,7 +381,9 @@ class CacheableLookup { const now = Date.now(); - for (const [hostname, {expires}] of this._cache) { + for (const [hostname, entries] of this._cache) { + const expires = entries[kExpires]; + if (now >= expires) { this._cache.delete(hostname); } else if (expires < nextExpiry) { @@ -346,8 +438,13 @@ class CacheableLookup { } updateInterfaceInfo() { + const {_iface} = this; + this._iface = getIfaceInfo(); - this._cache.clear(); + + if ((_iface.has4 && !this._iface.has4) || (_iface.has6 && !this._iface.has6)) { + this._cache.clear(); + } } clear(hostname) { diff --git a/tests/crlfHosts.txt b/tests/crlfHosts.txt deleted file mode 100644 index acec793..0000000 --- a/tests/crlfHosts.txt +++ /dev/null @@ -1,17 +0,0 @@ -# this is a comment -127.0.0.1 helloworld -127.0.0.1 foobar # this is a comment -# this is yet another comment 127.0.0.1 woofwoof -noiphere -127.0.0.1 # no host here -127.0.0.1 foo1 foo2 -127.0.0.1 foo3 -::1 foo3 -127.0.0.1 foo4 -127.0.0.2 foo4 -127.0.0.1 manywhitespaces - 127.0.0.1 startswithwhitespace -127.0.0.1 tab -0.0.0.0 DoNotDeleteThisLineForResetPurposesOnly - -127.0.0.1 doublenewline diff --git a/tests/hosts.txt b/tests/hosts.txt deleted file mode 100644 index acec793..0000000 --- a/tests/hosts.txt +++ /dev/null @@ -1,17 +0,0 @@ -# this is a comment -127.0.0.1 helloworld -127.0.0.1 foobar # this is a comment -# this is yet another comment 127.0.0.1 woofwoof -noiphere -127.0.0.1 # no host here -127.0.0.1 foo1 foo2 -127.0.0.1 foo3 -::1 foo3 -127.0.0.1 foo4 -127.0.0.2 foo4 -127.0.0.1 manywhitespaces - 127.0.0.1 startswithwhitespace -127.0.0.1 tab -0.0.0.0 DoNotDeleteThisLineForResetPurposesOnly - -127.0.0.1 doublenewline diff --git a/tests/test.js b/tests/test.js index ae1a75c..c3374c2 100644 --- a/tests/test.js +++ b/tests/test.js @@ -1,14 +1,11 @@ -const {V4MAPPED, ADDRCONFIG} = require('dns'); +const {V4MAPPED, ADDRCONFIG, ALL} = require('dns'); const {Resolver: AsyncResolver} = require('dns').promises; -const fs = require('fs').promises; const {promisify} = require('util'); const http = require('http'); -const path = require('path'); const test = require('ava'); const Keyv = require('keyv'); const proxyquire = require('proxyquire'); - -const hostsFiles = ['hosts.txt', 'crlfHosts.txt']; +const QuickLRU = require('quick-lru'); const makeRequest = options => new Promise((resolve, reject) => { http.get(options, resolve).once('error', reject); @@ -71,7 +68,11 @@ const mockedInterfaces = options => { }; const createResolver = () => { - let totalQueries = 0; + let counter = { + 4: 0, + 6: 0, + lookup: 0 + }; const resolver = { servers: ['127.0.0.1'], @@ -82,13 +83,6 @@ const createResolver = () => { resolver.servers = [...servers]; }, resolve: (hostname, options, callback) => { - totalQueries++; - - if (hostname === 'undefined') { - callback(new Error('no entry')); - return; - } - let data; for (const server of resolver.servers) { if (resolver.data[server][hostname]) { @@ -97,8 +91,27 @@ const createResolver = () => { } } + if (hostname === 'econnrefused') { + const error = new Error(`ECONNREFUSED ${hostname}`); + error.code = 'ECONNREFUSED'; + + callback(error); + return; + } + if (!data) { - callback(null, undefined); + const error = new Error(`ENOTFOUND ${hostname}`); + error.code = 'ENOTFOUND'; + + callback(error); + return; + } + + if (data.length === 0) { + const error = new Error(`ENODATA ${hostname}`); + error.code = 'ENODATA'; + + callback(error); return; } @@ -109,15 +122,19 @@ const createResolver = () => { callback(null, JSON.parse(JSON.stringify(data))); }, resolve4: (hostname, options, callback) => { + counter[4]++; + return resolver.resolve(hostname, {...options, family: 4}, callback); }, resolve6: (hostname, options, callback) => { + counter[6]++; + return resolver.resolve(hostname, {...options, family: 6}, callback); }, lookup: (hostname, options, callback) => { - // We don't need to implement hints here + // No need to implement hints yet - totalQueries++; + counter.lookup++; if (!resolver.lookupData[hostname]) { const error = new Error(`ENOTFOUND ${hostname}`); @@ -171,6 +188,9 @@ const createResolver = () => { multiple: [ {address: '127.0.0.127', family: 4, ttl: 0}, {address: '127.0.0.128', family: 4, ttl: 0} + ], + outdated: [ + {address: '127.0.0.1', family: 4, ttl: 1} ] }, '192.168.0.100': { @@ -183,10 +203,20 @@ const createResolver = () => { osHostname: [ {address: '127.0.0.1', family: 4}, {address: '127.0.0.2', family: 4} + ], + outdated: [ + {address: '127.0.0.127', family: 4} ] }, - get totalQueries() { - return totalQueries; + get counter() { + return counter; + }, + resetCounter() { + counter = { + 4: 0, + 6: 0, + lookup: 0 + }; } }; @@ -233,7 +263,7 @@ const verify = (t, entry, value) => { }; test('options.family', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); // IPv4 let entry = await cacheable.lookupAsync('localhost', {family: 4}); @@ -251,7 +281,7 @@ test('options.family', async t => { }); test('options.all', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); const entries = await cacheable.lookupAsync('localhost', {all: true}); verify(t, entries, [ @@ -261,7 +291,7 @@ test('options.all', async t => { }); test('options.all mixed with options.family', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); // IPv4 let entries = await cacheable.lookupAsync('localhost', {all: true, family: 4}); @@ -277,21 +307,42 @@ test('options.all mixed with options.family', async t => { }); test('V4MAPPED hint', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); // Make sure default behavior is right await t.throwsAsync(cacheable.lookupAsync('static4', {family: 6}), {code: 'ENOTFOUND'}); // V4MAPPED - const entries = await cacheable.lookupAsync('static4', {family: 6, hints: V4MAPPED}); - verify(t, entries, {address: '::ffff:127.0.0.1', family: 6}); + { + const entries = await cacheable.lookupAsync('static4', {family: 6, hints: V4MAPPED}); + verify(t, entries, {address: '::ffff:127.0.0.1', family: 6}); + } + + { + const entries = await cacheable.lookupAsync('localhost', {family: 6, hints: V4MAPPED}); + verify(t, entries, {address: '::ffff:127.0.0.2', family: 6}); + } }); +if (process.versions.node.split('.')[0] >= 14) { + test('ALL hint', async t => { + const cacheable = new CacheableLookup({resolver}); + + // ALL + const entries = await cacheable.lookupAsync('localhost', {family: 6, hints: V4MAPPED | ALL, all: true}); + + verify(t, entries, [ + {address: '::ffff:127.0.0.1', family: 6, ttl: 60}, + {address: '::ffff:127.0.0.2', family: 6, ttl: 60} + ]); + }); +} + test('ADDRCONFIG hint', async t => { //=> has6 = false, family = 6 { const CacheableLookup = mockedInterfaces({has4: true, has6: false}); - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); await t.throwsAsync(cacheable.lookupAsync('localhost', {family: 6, hints: ADDRCONFIG}), {code: 'ENOTFOUND'}); } @@ -299,7 +350,7 @@ test('ADDRCONFIG hint', async t => { //=> has6 = true, family = 6 { const CacheableLookup = mockedInterfaces({has4: true, has6: true}); - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); verify(t, await cacheable.lookupAsync('localhost', {family: 6, hints: ADDRCONFIG}), { address: '::ffff:127.0.0.2', @@ -310,7 +361,7 @@ test('ADDRCONFIG hint', async t => { //=> has4 = false, family = 4 { const CacheableLookup = mockedInterfaces({has4: false, has6: true}); - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); await t.throwsAsync(cacheable.lookupAsync('localhost', {family: 4, hints: ADDRCONFIG}), {code: 'ENOTFOUND'}); } @@ -318,7 +369,7 @@ test('ADDRCONFIG hint', async t => { //=> has4 = true, family = 4 { const CacheableLookup = mockedInterfaces({has4: true, has6: true}); - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); verify(t, await cacheable.lookupAsync('localhost', {family: 4, hints: ADDRCONFIG}), { address: '127.0.0.1', @@ -329,7 +380,7 @@ test('ADDRCONFIG hint', async t => { // Update interface info { const CacheableLookup = mockedInterfaces({has4: false, has6: true}); - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); await t.throwsAsync(cacheable.lookupAsync('localhost', {family: 4, hints: ADDRCONFIG}), {code: 'ENOTFOUND'}); @@ -345,7 +396,7 @@ test('ADDRCONFIG hint', async t => { }); test.serial('caching works', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); // Make sure default behavior is right let entries = await cacheable.lookupAsync('temporary', {all: true, family: 4}); @@ -369,7 +420,7 @@ test.serial('caching works', async t => { }); test('respects ttl', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); // Make sure default behavior is right let entries = await cacheable.lookupAsync('ttl', {all: true, family: 4}); @@ -396,13 +447,13 @@ test('respects ttl', async t => { }); test('throw when there are entries available but not for the requested family', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); await t.throwsAsync(cacheable.lookupAsync('static4', {family: 6}), {code: 'ENOTFOUND'}); }); test('custom servers', async t => { - const cacheable = new CacheableLookup({resolver: createResolver(), customHostsPath: false}); + const cacheable = new CacheableLookup({resolver: createResolver()}); // .servers (get) t.deepEqual(cacheable.servers, ['127.0.0.1']); @@ -420,7 +471,7 @@ test('custom servers', async t => { }); test('callback style', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); // Custom promise for this particular test const lookup = (...args) => new Promise((resolve, reject) => { @@ -448,7 +499,7 @@ test('callback style', async t => { }); test('works', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); verify(t, await cacheable.lookupAsync('localhost'), { address: '127.0.0.1', @@ -459,7 +510,7 @@ test('works', async t => { test('options.maxTtl', async t => { //=> maxTtl = 1 { - const cacheable = new CacheableLookup({resolver, maxTtl: 1, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver, maxTtl: 1}); // Make sure default behavior is right verify(t, await cacheable.lookupAsync('maxTtl'), { @@ -486,7 +537,7 @@ test('options.maxTtl', async t => { //=> maxTtl = 0 { - const cacheable = new CacheableLookup({resolver, maxTtl: 0, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver, maxTtl: 0}); // Make sure default behavior is right verify(t, await cacheable.lookupAsync('maxTtl'), { @@ -513,7 +564,7 @@ test('options.maxTtl', async t => { }); test('entry with 0 ttl', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); // Make sure default behavior is right verify(t, await cacheable.lookupAsync('zeroTtl'), { @@ -532,7 +583,7 @@ test('entry with 0 ttl', async t => { }); test('http example', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); const options = { hostname: 'example', @@ -546,7 +597,7 @@ test('http example', async t => { }); test('.lookup() and .lookupAsync() are automatically bounded', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); await t.notThrowsAsync(cacheable.lookupAsync('localhost')); await t.notThrowsAsync(promisify(cacheable.lookup)('localhost')); @@ -557,7 +608,7 @@ test('.lookup() and .lookupAsync() are automatically bounded', async t => { }); test('works (Internet connection)', async t => { - const cacheable = new CacheableLookup({customHostsPath: false}); + const cacheable = new CacheableLookup(); const {address, family} = await cacheable.lookupAsync('1dot1dot1dot1.cloudflare-dns.com'); t.true(address === '1.1.1.1' || address === '1.0.0.1'); @@ -565,7 +616,7 @@ test('works (Internet connection)', async t => { }); test.serial('install & uninstall', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); cacheable.install(http.globalAgent); const options = { @@ -587,7 +638,7 @@ test.serial('install & uninstall', async t => { }); test('`.install()` throws if no Agent provided', t => { - const cacheable = new CacheableLookup({customHostsPath: false}); + const cacheable = new CacheableLookup(); t.throws(() => cacheable.install(), { message: 'Expected an Agent instance as the first argument' @@ -599,7 +650,7 @@ test('`.install()` throws if no Agent provided', t => { }); test('`.uninstall()` throws if no Agent provided', t => { - const cacheable = new CacheableLookup({customHostsPath: false}); + const cacheable = new CacheableLookup(); t.throws(() => cacheable.uninstall(), { message: 'Expected an Agent instance as the first argument' @@ -611,7 +662,7 @@ test('`.uninstall()` throws if no Agent provided', t => { }); test.serial('`.uninstall()` does not alter unmodified Agents', t => { - const cacheable = new CacheableLookup({customHostsPath: false}); + const cacheable = new CacheableLookup(); const {createConnection} = http.globalAgent; cacheable.uninstall(http.globalAgent); @@ -620,7 +671,7 @@ test.serial('`.uninstall()` does not alter unmodified Agents', t => { }); test.serial('throws if double-installing CacheableLookup', t => { - const cacheable = new CacheableLookup({customHostsPath: false}); + const cacheable = new CacheableLookup(); cacheable.install(http.globalAgent); t.throws(() => cacheable.install(http.globalAgent), { @@ -650,8 +701,8 @@ test.serial('install - providing custom lookup function anyway', async t => { }); test.serial('throws when calling `.uninstall()` on the wrong instance', t => { - const a = new CacheableLookup({customHostsPath: false}); - const b = new CacheableLookup({resolver, customHostsPath: false}); + const a = new CacheableLookup(); + const b = new CacheableLookup({resolver}); a.install(http.globalAgent); @@ -663,7 +714,7 @@ test.serial('throws when calling `.uninstall()` on the wrong instance', t => { }); test('async resolver (Internet connection)', async t => { - const cacheable = new CacheableLookup({resolver: new AsyncResolver(), customHostsPath: false}); + const cacheable = new CacheableLookup({resolver: new AsyncResolver()}); t.is(typeof cacheable._resolve4, 'function'); t.is(typeof cacheable._resolve6, 'function'); @@ -673,7 +724,7 @@ test('async resolver (Internet connection)', async t => { }); test('clear() works', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); await cacheable.lookupAsync('localhost'); t.is(cacheable._cache.size, 1); @@ -684,7 +735,7 @@ test('clear() works', async t => { }); test('ttl works', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); await Promise.all([cacheable.lookupAsync('temporary'), cacheable.lookupAsync('ttl')]); t.is(cacheable._cache.size, 2); @@ -694,101 +745,10 @@ test('ttl works', async t => { t.is(cacheable._cache.size, 0); }); -for (const file of hostsFiles) { - test(`respects the \`hosts\` file - ${file}`, async t => { - const cacheable = new CacheableLookup({ - customHostsPath: path.resolve(__dirname, file) - }); - - const getAddress = async hostname => { - const result = await cacheable.lookupAsync(hostname); - - t.is(result.family, 4); - t.is(result.ttl, Infinity); - t.is(result.expires, Infinity); - return result.address; - }; - - t.is(await getAddress('helloworld'), '127.0.0.1'); - t.is(await getAddress('foobar'), '127.0.0.1'); - await t.throwsAsync(getAddress('woofwoof'), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('noiphere'), {code: 'ENOTFOUND'}); - t.is(await getAddress('foo1'), '127.0.0.1'); - t.is(await getAddress('foo2'), '127.0.0.1'); - t.is(await getAddress('manywhitespaces'), '127.0.0.1'); - t.is(await getAddress('startswithwhitespace'), '127.0.0.1'); - t.is(await getAddress('tab'), '127.0.0.1'); - t.is(await getAddress('doublenewline'), '127.0.0.1'); - - { - const entry = await cacheable.lookupAsync('foo3', {family: 4}); - t.is(entry.address, '127.0.0.1'); - t.is(entry.family, 4); - t.is(entry.expires, Infinity); - t.is(entry.ttl, Infinity); - } - - { - const entry = await cacheable.lookupAsync('foo3', {family: 6}); - t.is(entry.address, '::1'); - t.is(entry.family, 6); - t.is(entry.expires, Infinity); - t.is(entry.ttl, Infinity); - } - - { - const entries = await cacheable.lookupAsync('foo4', {all: true}); - t.deepEqual(entries, [ - { - address: '127.0.0.1', - family: 4, - expires: Infinity, - ttl: Infinity - } - ]); - } - }); - - test(`the \`hosts\` file support can be turned off - ${file}`, async t => { - const cacheable = new CacheableLookup({ - customHostsPath: false, - resolver - }); - - const getAddress = async hostname => { - const result = await cacheable.lookupAsync(hostname); - - t.is(result.family, 4); - t.is(result.ttl, Infinity); - t.is(result.expires, Infinity); - - return result.address; - }; - - await t.throwsAsync(getAddress('helloworld'), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('foobar'), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('woofwoof'), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('noiphere'), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('foo1'), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('foo2'), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('manywhitespaces'), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('startswithwhitespace'), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('tab'), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('doublenewline'), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('foo3', {family: 4}), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('foo3', {family: 6}), {code: 'ENOTFOUND'}); - await t.throwsAsync(getAddress('foo4', {all: true}), {code: 'ENOTFOUND'}); - - const {address} = await cacheable.lookupAsync('localhost'); - t.is(address, '127.0.0.1'); - }); -} - test('custom cache support', async t => { const cache = new Keyv(); const cacheable = new CacheableLookup({ - customHostsPath: false, resolver, cache }); @@ -803,52 +763,14 @@ test('custom cache support', async t => { await sleep(entry.ttl * 1001); - cacheable.tick(); - const newEntry = await cache.get('temporary'); t.is(newEntry, undefined); }); -test('travis hosts', async t => { - const resolver = createResolver(); - resolver.data = {}; - - const cacheable = new CacheableLookup({ - customHostsPath: path.resolve(__dirname, 'travisHosts.txt'), - resolver - }); - - const entry = await cacheable.lookupAsync('localhost'); - - t.deepEqual(entry, { - address: '127.0.0.1', - expires: Infinity, - family: 4, - ttl: Infinity - }); -}); - -test('lookup throws if failed to retrieve the `hosts` file', async t => { - const resolver = createResolver(); - resolver.data = {}; - - const cacheable = new CacheableLookup({ - customHostsPath: path.resolve(__dirname, 'doesNotExist.txt'), - resolver - }); - - await t.throwsAsync( - cacheable.lookupAsync('localhost'), - { - code: 'ENOENT', - message: /^ENOENT: no such file or directory/ - } - ); -}); - -test('fallback works', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false, fallbackTtl: 1}); +test.serial('fallback works', async t => { + const cacheable = new CacheableLookup({resolver, fallbackDuration: 3600}); + resolver.resetCounter(); const entries = await cacheable.lookupAsync('osHostname', {all: true}); t.is(entries.length, 2); @@ -859,17 +781,52 @@ test('fallback works', async t => { t.is(entries[1].address, '127.0.0.2'); t.is(entries[1].family, 4); - t.is(cacheable._cache.size, 1); + t.is(cacheable._cache.size, 0); - await sleep((entries[0].ttl * 1000) + 1); + await cacheable.lookupAsync('osHostname', {all: true}); - cacheable.tick(); + t.deepEqual(resolver.counter, { + 6: 1, + 4: 1, + lookup: 2 + }); +}); - t.is(cacheable._cache.size, 0); +test('fallback works #2', async t => { + const resolver = createResolver({delay: 0}); + const cacheable = new CacheableLookup({ + resolver, + fallbackDuration: 3600, + lookup: resolver.lookup + }); + + { + const entries = await cacheable.lookupAsync('outdated', {all: true}); + t.deepEqual(entries, [ + {address: '127.0.0.127', family: 4} + ]); + } + + await new Promise(resolve => setTimeout(resolve, 100)); + + { + const entries = await cacheable.lookupAsync('outdated', {all: true}); + verify(t, entries, [ + {address: '127.0.0.1', family: 4} + ]); + } +}); + +test('fallback can be turned off', async t => { + const cacheable = new CacheableLookup({resolver, fallbackDuration: 0}); + + await t.throwsAsync(cacheable.lookupAsync('osHostname', {all: true}), { + message: 'cacheableLookup ENOTFOUND osHostname' + }); }); test('errors are cached', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false, errorTtl: 0.1}); + const cacheable = new CacheableLookup({resolver, errorTtl: 0.1}); await t.throwsAsync(cacheable.lookupAsync('doesNotExist'), { code: 'ENOTFOUND' @@ -879,13 +836,11 @@ test('errors are cached', async t => { await sleep((cacheable.errorTtl * 1000) + 1); - cacheable.tick(); - t.is(cacheable._cache.size, 0); }); test('passing family as options', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); const promisified = promisify(cacheable.lookup); @@ -898,7 +853,7 @@ test('passing family as options', async t => { }); test('clear(hostname) works', async t => { - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); await cacheable.lookupAsync('localhost'); await cacheable.lookupAsync('temporary'); @@ -910,26 +865,23 @@ test('clear(hostname) works', async t => { test('prevents overloading DNS', async t => { const resolver = createResolver(); - const {lookupAsync} = new CacheableLookup({resolver, customHostsPath: false}); + const {lookupAsync} = new CacheableLookup({ + resolver, + lookup: resolver.lookup + }); await Promise.all([lookupAsync('localhost'), lookupAsync('localhost')]); - t.is(resolver.totalQueries, 2); -}); - -test('one HostsResolver per hosts file', t => { - const customHostsPath = path.resolve(__dirname, 'hosts.txt'); - const resolver = createResolver(); - - const first = new CacheableLookup({customHostsPath, resolver}); - const second = new CacheableLookup({customHostsPath, resolver}); - - t.is(first._hostsResolver, second._hostsResolver); + t.deepEqual(resolver.counter, { + 4: 1, + 6: 1, + lookup: 1 + }); }); test('returns IPv6 if no other entries available', async t => { const CacheableLookup = mockedInterfaces({has4: false, has6: true}); - const cacheable = new CacheableLookup({resolver, customHostsPath: false}); + const cacheable = new CacheableLookup({resolver}); verify(t, await cacheable.lookupAsync('localhost', {hints: ADDRCONFIG}), { address: '::ffff:127.0.0.2', @@ -937,33 +889,72 @@ test('returns IPv6 if no other entries available', async t => { }); }); -test('hosts file watcher works', async t => { - const customHostsPath = path.resolve(__dirname, 'watching.txt'); +test('throws when no internet connection', async t => { + const cacheable = new CacheableLookup({resolver}); - await fs.writeFile(customHostsPath, '127.0.0.1 demo'); + await t.throwsAsync(cacheable.lookupAsync('econnrefused'), { + code: 'ECONNREFUSED' + }); +}); + +test('full-featured custom cache', async t => { + const cache = new QuickLRU({maxSize: 1}); const cacheable = new CacheableLookup({ resolver, - customHostsPath, - watchingHostsFile: true + cache }); - const first = await cacheable.lookupAsync('demo', {all: true}); + { + const entry = await cacheable.lookupAsync('localhost'); - await fs.writeFile(customHostsPath, ''); + verify(t, entry, { + address: '127.0.0.1', + family: 4 + }); + } - await new Promise(resolve => setTimeout(resolve, 100)); + t.is(cache.size, 1); - await t.throwsAsync(cacheable.lookupAsync('demo', {all: true}), { - code: 'ENOTFOUND' - }); + { + const entry = await cacheable.lookupAsync('localhost'); - t.deepEqual(first, [ - { + verify(t, entry, { address: '127.0.0.1', - family: 4, - ttl: Infinity, - expires: Infinity + family: 4 + }); + } + + t.is(cache.size, 1); + + { + const entry = await cacheable.lookupAsync('temporary'); + + verify(t, entry, { + address: '127.0.0.1', + family: 4 + }); + } + + t.is(cache.size, 1); +}); + +test('throws when the cache instance is broken', async t => { + const cacheable = new CacheableLookup({ + resolver, + cache: { + get: () => {}, + set: () => { + throw new Error('Something broke.'); + } } - ]); + }); + + await t.notThrowsAsync(cacheable.lookupAsync('localhost')); + + const error = await t.throwsAsync(cacheable.lookupAsync('localhost'), { + message: 'Cache Error. Please recreate the CacheableLookup instance.' + }); + + t.is(error.cause.message, 'Something broke.'); }); diff --git a/tests/travisHosts.txt b/tests/travisHosts.txt deleted file mode 100644 index 884c7fd..0000000 --- a/tests/travisHosts.txt +++ /dev/null @@ -1,5 +0,0 @@ -## Managed by Chef on packer-5c98fea1-aa3f-407c-10e9-32c3c213ffca.c.eco-emissary-99515.internal :heart_eyes_cat: -## cookbook:: travis_build_environment -## file:: templates/default/etc/cloud/templates/hosts.tmpl.erb -127.0.1.1 travis-job-e777c4e7-efdd-4a08-a20f-d25703d8702f travis-job-e777c4e7-efdd-4a08-a20f-d25703d8702f ip4-loopback xenial64 -127.0.0.1 localhost nettuno travis vagrant travis-job-e777c4e7-efdd-4a08-a20f-d25703d8702f diff --git a/tests/watching.txt b/tests/watching.txt deleted file mode 100644 index e69de29..0000000