diff --git a/__mocks__/https.js b/__mocks__/https.js index a4cee94..184f530 100644 --- a/__mocks__/https.js +++ b/__mocks__/https.js @@ -10,7 +10,7 @@ https.get.mockImplementation((url, options, callback) => { stream.emit( "data", Buffer.from( - `{"google.com": {"url":"google.com","hosted_by":"Google Inc.","hosted_by_website":"https://www.google.com","partner":null,"green":true}}` + `{"google.com": {"url":"google.com","hosted_by":"Google Inc.","hosted_by_website":"https://www.google.com","partner":null,"green":true}, "pchome.com": {"url":"pchome.com","green":false} }` ) ); } else { diff --git a/src/helpers/index.js b/src/helpers/index.js index 29db234..1d0bc1a 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -5,6 +5,16 @@ import { FIRST_TIME_VIEWING_PERCENTAGE, RETURNING_VISITOR_PERCENTAGE, } from "../constants/index.js"; + +// Shared type definitions to be used across different files + +/** + * @typedef {Object} DomainCheckOptions options to control the behavior when checking a domain + * @property {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. + * @property {boolean} verbose - Optional. Whether to return a verbose response. + * @property {string[]} db - Optional. A database list to use for lookups. + */ + const formatNumber = (num) => parseFloat(num.toFixed(2)); function parseOptions(options) { diff --git a/src/hosting-api.js b/src/hosting-api.js index 60337a7..4e62c88 100644 --- a/src/hosting-api.js +++ b/src/hosting-api.js @@ -1,60 +1,77 @@ "use strict"; import { getApiRequestHeaders } from "./helpers/index.js"; +import hostingJSON from "./hosting-json.js"; /** - * Check if a string or array of domains has been provided + * Check if a string or array of domains is hosted by a green web host by querying the Green Web Foundation API. * @param {string|array} domain - The domain to check, or an array of domains to be checked. - * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. + * @param {string | DomainCheckOptions} optionsOrAgentId - Optional. An object of domain check options, or a string + * representing the app, site, or organisation that is making the request. */ -function check(domain, userAgentIdentifier) { +function check(domain, optionsOrAgentId) { + const options = + typeof optionsOrAgentId === "string" + ? { userAgentIdentifier: optionsOrAgentId } + : optionsOrAgentId; + + if (options?.db && options.verbose) { + throw new Error("verbose mode cannot be used with a local lookup database"); + } // is it a single domain or an array of them? if (typeof domain === "string") { - return checkAgainstAPI(domain, userAgentIdentifier); + return checkAgainstAPI(domain, options); } else { - return checkDomainsAgainstAPI(domain, userAgentIdentifier); + return checkDomainsAgainstAPI(domain, options); } } /** * Check if a domain is hosted by a green web host by querying the Green Web Foundation API. * @param {string} domain - The domain to check. - * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. - * @returns {boolean} - A boolean indicating whether the domain is hosted by a green web host. + * @param {DomainCheckOptions} options + * @returns - A boolean indicating whether the domain is hosted by a green web host if `options.verbose` is false, + * otherwise an object representing the domain host information. */ -async function checkAgainstAPI(domain, userAgentIdentifier) { +async function checkAgainstAPI(domain, options = {}) { const req = await fetch( `https://api.thegreenwebfoundation.org/greencheck/${domain}`, { - headers: getApiRequestHeaders(userAgentIdentifier), + headers: getApiRequestHeaders(options.userAgentIdentifier), } ); + if (options?.db) { + return hostingJSON.check(domain, options.db); + } const res = await req.json(); - return res.green; + return options.verbose ? res : res.green; } /** * Check if an array of domains is hosted by a green web host by querying the Green Web Foundation API. * @param {array} domains - An array of domains to check. - * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. - * @returns {array} - An array of domains that are hosted by a green web host. + * @param {DomainCheckOptions} options + * @returns - An array of domains that are hosted by a green web host if `options.verbose` is false, + * otherwise a dictionary of domain to host information. */ -async function checkDomainsAgainstAPI(domains, userAgentIdentifier) { +async function checkDomainsAgainstAPI(domains, options = {}) { try { const apiPath = "https://api.thegreenwebfoundation.org/v2/greencheckmulti"; const domainsString = JSON.stringify(domains); const req = await fetch(`${apiPath}/${domainsString}`, { - headers: getApiRequestHeaders(userAgentIdentifier), + headers: getApiRequestHeaders(options.userAgentIdentifier), }); const allGreenCheckResults = await req.json(); - return greenDomainsFromResults(allGreenCheckResults); + return options.verbose + ? allGreenCheckResults + : greenDomainsFromResults(allGreenCheckResults); } catch (e) { - return []; + return options.verbose ? {} : []; } } diff --git a/src/hosting-api.test.js b/src/hosting-api.test.js index fc37294..e1c3616 100644 --- a/src/hosting-api.test.js +++ b/src/hosting-api.test.js @@ -40,6 +40,33 @@ describe("hostingAPI", () => { ); expect(res).toEqual(true); }); + it("handles the verbose=true option", async () => { + fetch.mockImplementation(() => + Promise.resolve({ + json: () => + Promise.resolve({ + url: "google.com", + hosted_by: "Google Inc.", + hosted_by_website: "https://www.google.com", + green: true, + }), + }) + ); + const res = await hosting.check("google.com", { verbose: true }); + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenLastCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { "User-Agent": "co2js/1.2.34 " }, + }) + ); + expect(res).toMatchObject({ + green: true, + hosted_by: "Google Inc.", + hosted_by_website: "https://www.google.com", + url: "google.com", + }); + }); }); describe("implicitly checking multiple domains with #check", () => { it("using the API", async () => { @@ -69,6 +96,41 @@ describe("hostingAPI", () => { ); expect(res).toContain("google.com"); }); + it("handles the verbose=true option", async () => { + fetch.mockImplementation(() => + Promise.resolve({ + json: () => + Promise.resolve({ + "google.com": { + url: "google.com", + hosted_by: "Google Inc.", + hosted_by_website: "https://www.google.com", + green: true, + }, + "kochindustries.com": { + url: "kochindustries.com", + green: false, + }, + }), + }) + ); + const res = await hosting.check(["google.com", "kochindustries.com"], { + verbose: true, + }); + expect(fetch).toHaveBeenCalledTimes(1); + expect(res).toEqual({ + "google.com": expect.objectContaining({ + green: true, + hosted_by: "Google Inc.", + hosted_by_website: "https://www.google.com", + url: "google.com", + }), + "kochindustries.com": expect.objectContaining({ + url: "kochindustries.com", + green: false, + }), + }); + }); }); }); /* eslint-enable jest/no-disabled-tests */ diff --git a/src/hosting-json.js b/src/hosting-json.js new file mode 100644 index 0000000..46aaf51 --- /dev/null +++ b/src/hosting-json.js @@ -0,0 +1,105 @@ +"use strict"; + +/** + * Check if a string or array of domains has been provided + * @param {string|array} domain - The domain to check, or an array of domains to be checked. + */ +async function check(domain, db) { + // is it a single domain or an array of them? + if (typeof domain === "string") { + return checkInJSON(domain, db); + } else { + return checkDomainsInJSON(domain, db); + } +} + +/** + * Check if a domain is hosted by a green web host by querying the database. + * @param {string} domain - The domain to check. + * @param {object} db - The database to check against. + * @returns {boolean} - A boolean indicating whether the domain is hosted by a green web host. + */ +function checkInJSON(domain, db) { + if (db.indexOf(domain) > -1) { + return true; + } + return false; +} + +/** + * Extract the green domains from the results of a green check. + * @param {object} greenResults - The results of a green check. + * @returns {array} - An array of domains that are hosted by a green web host. + */ +function greenDomainsFromResults(greenResults) { + const entries = Object.entries(greenResults); + const greenEntries = entries.filter(([key, val]) => val.green); + + return greenEntries.map(([key, val]) => val.url); +} + +/** + * Check if an array of domains is hosted by a green web host by querying the database. + * @param {array} domains - An array of domains to check. + * @param {object} db - The database to check against. + * @returns {array} - An array of domains that are hosted by a green web host. + */ +function checkDomainsInJSON(domains, db) { + let greenDomains = []; + + for (let domain of domains) { + if (db.indexOf(domain) > -1) { + greenDomains.push(domain); + } + } + return greenDomains; +} + +/** + * Find the provided information a string or array of domains + * @param {string|array} domain - The domain to check, or an array of domains to be checked. + */ +function find(domain, db) { + // is it a single domain or an array of them? + if (typeof domain === "string") { + return findInJSON(domain, db); + } else { + return findDomainsInJSON(domain, db); + } +} + +/** + * Check if a domain is hosted by a green web host by querying the database. + * @param {string} domain - The domain to check. + * @param {object} db - The database to check against. + * @returns {object} - An object representing the domain provided host information. + */ +function findInJSON(domain, db) { + if (db.indexOf(domain) > -1) { + return domain; + } + return { + url: domain, + green: false, + }; +} + +/** + * Check if an array of domains is hosted by a green web host by querying the database. + * @param {array} domains - An array of domains to check. + * @param {object} db - The database to check against. + * @returns {array} - A dictionary of domain to provided host information. + */ +function findDomainsInJSON(domains, db) { + const result = {}; + for (let domain of domains) { + result[domain] = findInJSON(domain, db); + } + return result; +} + +module.exports = { + check, + greenDomainsFromResults, + find, +}; diff --git a/src/hosting-json.node.js b/src/hosting-json.node.js index 6f11a0f..c264939 100644 --- a/src/hosting-json.node.js +++ b/src/hosting-json.node.js @@ -45,63 +45,6 @@ async function loadJSON(jsonPath) { return JSON.parse(jsonBuffer); } -/** - * Check if a string or array of domains has been provided - * @param {string|array} domain - The domain to check, or an array of domains to be checked. - */ -async function check(domain, db) { - // is it a single domain or an array of them? - if (typeof domain === "string") { - return checkInJSON(domain, db); - } else { - return checkDomainsInJSON(domain, db); - } -} - -/** - * Check if a domain is hosted by a green web host by querying the database. - * @param {string} domain - The domain to check. - * @param {object} db - The database to check against. - * @returns {boolean} - A boolean indicating whether the domain is hosted by a green web host. - */ -function checkInJSON(domain, db) { - if (db.indexOf(domain) > -1) { - return true; - } - return false; -} - -/** - * Extract the green domains from the results of a green check. - * @param {object} greenResults - The results of a green check. - * @returns {array} - An array of domains that are hosted by a green web host. - */ -function greenDomainsFromResults(greenResults) { - const entries = Object.entries(greenResults); - const greenEntries = entries.filter(([key, val]) => val.green); - - return greenEntries.map(([key, val]) => val.url); -} - -/** - * Check if an array of domains is hosted by a green web host by querying the database. - * @param {array} domains - An array of domains to check. - * @param {object} db - The database to check against. - * @returns {array} - An array of domains that are hosted by a green web host. - */ -function checkDomainsInJSON(domains, db) { - let greenDomains = []; - - for (let domain of domains) { - if (db.indexOf(domain) > -1) { - greenDomains.push(domain); - } - } - return greenDomains; -} - module.exports = { - check, loadJSON, - greenDomainsFromResults, }; diff --git a/src/hosting-json.node.test.js b/src/hosting-json.node.test.js index 270f58e..7c62a47 100644 --- a/src/hosting-json.node.test.js +++ b/src/hosting-json.node.test.js @@ -1,6 +1,7 @@ "use strict"; -import hosting from "./hosting-json.node.js"; +import hosting from "./hosting-json.js"; +import hostingNode from "./hosting-json.node.js"; import path from "path"; describe("hostingJSON", () => { @@ -20,19 +21,19 @@ describe("hostingJSON", () => { ); describe("checking a single domain with #check", () => { test("against the list of domains as JSON", async () => { - const db = await hosting.loadJSON(jsonPath); + const db = await hostingNode.loadJSON(jsonPath); const res = await hosting.check("google.com", db); expect(res).toEqual(true); }); test("against the list of domains as JSON loaded from a gzipped JSON", async () => { - const db = await hosting.loadJSON(jsonPathGz); + const db = await hostingNode.loadJSON(jsonPathGz); const res = await hosting.check("google.com", db); expect(res).toEqual(true); }); }); describe("implicitly checking multiple domains with #check", () => { test("against the list of domains as JSON", async () => { - const db = await hosting.loadJSON(jsonPath); + const db = await hostingNode.loadJSON(jsonPath); const domains = ["google.com", "kochindustries.com"]; const res = await hosting.check(domains, db); diff --git a/src/hosting-node.js b/src/hosting-node.js index 87eadfb..9ab4d40 100644 --- a/src/hosting-node.js +++ b/src/hosting-node.js @@ -10,7 +10,8 @@ This lets us keep the total library small, and dependencies minimal. import https from "https"; -import hostingJSON from "./hosting-json.node.js"; +import hostingJSON from "./hosting-json.js"; +import hostingJSONNode from "./hosting-json.node.js"; import { getApiRequestHeaders } from "./helpers/index.js"; /** @@ -51,59 +52,82 @@ async function getBody(url, userAgentIdentifier) { /** * Check if a domain is hosted by a green web host. * @param {string|array} domain - The domain to check, or an array of domains to be checked. - * @param {object} db - Optional. A database object to use for lookups. - * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. - * @returns {boolean|array} - A boolean if a string was provided, or an array of booleans if an array of domains was provided. + * @param {string[] | DomainCheckOptions} optionsOrDb - Optional. An object of domain check options, or a database list to use for lookups. + * @param {string | DomainCheckOptions} optionsOrAgentId - Optional. An object of domain check options, or a string + * representing the app, site, or organisation that is making the request. + * @returns - A boolean if a string was provided, or an array of booleans if an array of domains was provided. + * if a string was provided for `domain`: a boolean indicating whether the domain is hosted by a green web host if `options.verbose` is false, + * otherwise an object representing the domain host information. + * if an array was provided for `domain`: an array of domains that are hosted by a green web host if `options.verbose` is false, + * otherwise a dictionary of domain to host information. */ -function check(domain, db, userAgentIdentifier) { +function check(domain, optionsOrDb, optionsOrAgentId) { + let db, options; + if (!db || Array.isArray(optionsOrDb)) { + db = optionsOrDb; + options = + typeof optionsOrAgentId === "string" + ? { userAgentIdentifier: optionsOrAgentId } + : optionsOrAgentId; + } else { + options = optionsOrDb; + db = optionsOrDb.db; + } + + if (db && options?.verbose) { + throw new Error("verbose mode cannot be used with a local lookup database"); + } if (db) { return hostingJSON.check(domain, db); } - // is it a single domain or an array of them? if (typeof domain === "string") { - return checkAgainstAPI(domain, userAgentIdentifier); + return checkAgainstAPI(domain, options); } else { - return checkDomainsAgainstAPI(domain, userAgentIdentifier); + return checkDomainsAgainstAPI(domain, options); } } /** * Check if a domain is hosted by a green web host by querying the Green Web Foundation API. * @param {string} domain - The domain to check. - * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. - * @returns {boolean} - A boolean indicating whether the domain is hosted by a green web host. + * @param {DomainCheckOptions} options + * @returns {boolean} - A boolean indicating whether the domain is hosted by a green web host if `options.verbose` is false, + * otherwise an object representing the domain host information. */ -async function checkAgainstAPI(domain, userAgentIdentifier) { +async function checkAgainstAPI(domain, options = {}) { const res = JSON.parse( await getBody( `https://api.thegreenwebfoundation.org/greencheck/${domain}`, - userAgentIdentifier + options.userAgentIdentifier ) ); - return res.green; + return options.verbose ? res : res.green; } /** * Check if an array of domains is hosted by a green web host by querying the Green Web Foundation API. * @param {array} domains - An array of domains to check. - * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. - * @returns {array} - An array of domains that are hosted by a green web host. + * @param {DomainCheckOptions} options + * @returns {array} - An array of domains that are hosted by a green web host if `options.verbose` is false, + * otherwise a dictionary of domain to host information. */ -async function checkDomainsAgainstAPI(domains, userAgentIdentifier) { +async function checkDomainsAgainstAPI(domains, options = {}) { try { const allGreenCheckResults = JSON.parse( await getBody( `https://api.thegreenwebfoundation.org/v2/greencheckmulti/${JSON.stringify( domains )}`, - userAgentIdentifier + options.userAgentIdentifier ) ); - return hostingJSON.greenDomainsFromResults(allGreenCheckResults); + return options.verbose + ? allGreenCheckResults + : hostingJSON.greenDomainsFromResults(allGreenCheckResults); } catch (e) { - return []; + return options.verbose ? {} : []; } } @@ -122,5 +146,5 @@ export default { check, checkPage, greendomains: hostingJSON.greenDomainsFromResults, - loadJSON: hostingJSON.loadJSON, + loadJSON: hostingJSONNode.loadJSON, }; diff --git a/src/hosting.js b/src/hosting.js index d0020eb..b8e3da5 100644 --- a/src/hosting.js +++ b/src/hosting.js @@ -5,11 +5,16 @@ import hostingAPI from "./hosting-api.js"; /** * Check if a domain is hosted by a green web host. * @param {string|array} domain - The domain to check, or an array of domains to be checked. - * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. - * @returns {boolean|array} - A boolean if a string was provided, or an array of booleans if an array of domains was provided. + * @param {string} optionsOrAgentId - Optional. An object of domain check options, or a string + * representing the app, site, or organisation that is making the request. + * @returns - A boolean if a string was provided, or an array of booleans if an array of domains was provided. + * if a string was provided for `domain`: a boolean indicating whether the domain is hosted by a green web host if `options.verbose` is false, + * otherwise an object representing the domain host information. + * if an array was provided for `domain`: an array of domains that are hosted by a green web host if `options.verbose` is false, + * otherwise a dictionary of domain to host information. */ -function check(domain, userAgentIdentifier) { - return hostingAPI.check(domain, userAgentIdentifier); +function check(domain, optionsOrAgentId) { + return hostingAPI.check(domain, optionsOrAgentId); } export default { diff --git a/src/hosting.test.js b/src/hosting.test.js index 666eb83..9bb2b67 100644 --- a/src/hosting.test.js +++ b/src/hosting.test.js @@ -65,6 +65,17 @@ describe("hosting", () => { const res = await hosting.check("google.com"); expect(res).toEqual(true); }); + it("use the API instead with verbose=true", async () => { + const res = await hosting.check("google.com", null, { + verbose: true, + }); + expect(res).toMatchObject({ + green: true, + hosted_by: "Google Inc.", + hosted_by_website: "https://www.google.com", + url: "google.com", + }); + }); it("sets the correct user agent header", async () => { await hosting.check("google.com", null, requestHeaderComment); expect(httpsGetSpy).toHaveBeenCalledTimes(1); @@ -82,5 +93,23 @@ describe("hosting", () => { const res = await hosting.check(["google.com", "pchome.com"]); expect(res).toContain("google.com"); }); + + it("use the API with verbose=true", async () => { + const res = await hosting.check(["google.com", "pchome.com"], null, { + verbose: true, + }); + expect(res).toEqual({ + "google.com": expect.objectContaining({ + green: true, + hosted_by: "Google Inc.", + hosted_by_website: "https://www.google.com", + url: "google.com", + }), + "pchome.com": expect.objectContaining({ + url: "pchome.com", + green: false, + }), + }); + }); }); });