diff --git a/__mocks__/https.js b/__mocks__/https.js index a4cee948..184f5302 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/package-lock.json b/package-lock.json index 03c912d1..22a06ded 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "jest": "^28.1.0", "nock": "^13.2.4", "np": "^8.0.4", - "pagexray": "^4.4.2", "prettier": "^2.6.2" }, "engines": { @@ -9353,21 +9352,6 @@ "integrity": "sha512-DPBNWSUWC0wPofXeNThao0uP4a93J7r90UyhagmJS0QcacTTkorZwXYsOop70phn1hKdcf/2e9lJIhazS8bx5A==", "dev": true }, - "node_modules/pagexray": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/pagexray/-/pagexray-4.4.2.tgz", - "integrity": "sha512-Cw1WhyuqEy4nACgUMzTfuZn7EcQrsq4ozpTnkVlsGjm4eMbdGXKN8QwflFi1g7p2FpgSq1ElKgTGFrzVNdbEyQ==", - "dev": true, - "dependencies": { - "minimist": "1.2.6" - }, - "bin": { - "pagexray": "bin/index.js" - }, - "engines": { - "node": ">=8.9.0" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -19024,15 +19008,6 @@ "integrity": "sha512-DPBNWSUWC0wPofXeNThao0uP4a93J7r90UyhagmJS0QcacTTkorZwXYsOop70phn1hKdcf/2e9lJIhazS8bx5A==", "dev": true }, - "pagexray": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/pagexray/-/pagexray-4.4.2.tgz", - "integrity": "sha512-Cw1WhyuqEy4nACgUMzTfuZn7EcQrsq4ozpTnkVlsGjm4eMbdGXKN8QwflFi1g7p2FpgSq1ElKgTGFrzVNdbEyQ==", - "dev": true, - "requires": { - "minimist": "1.2.6" - } - }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/src/helpers/index.js b/src/helpers/index.js index 29db234d..1d0bc1a4 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 60337a7f..4e62c886 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 fc37294d..e1c36160 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 00000000..46aaf519 --- /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 6f11a0f0..c2649398 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 270f58e4..7c62a47e 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 064e207e..e1ce68a5 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,64 +52,88 @@ 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 } userAgentIdentifier - Optional. 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, userAgentIdentifier) { + let db, + options = {}; + if (!db && Array.isArray(optionsOrDb)) { + db = optionsOrDb; + } else { + options = optionsOrDb; + if (userAgentIdentifier) { + options = { ...options, userAgentIdentifier }; + } + db = optionsOrDb?.db; + } + + console.log({ db, options }); + + 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 ? {} : []; } } export default { check, greendomains: hostingJSON.greenDomainsFromResults, - loadJSON: hostingJSON.loadJSON, + loadJSON: hostingJSONNode.loadJSON, }; diff --git a/src/hosting.js b/src/hosting.js index d0020eb5..b8e3da5d 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 235fc135..e80ee1d8 100644 --- a/src/hosting.test.js +++ b/src/hosting.test.js @@ -19,14 +19,13 @@ const jsonPath = path.resolve( ); describe("hosting", () => { - let har; let httpsGetSpy; beforeEach(() => { httpsGetSpy = jest.spyOn(https, "get"); jest.clearAllMocks(); }); - describe("checking all domains on a page object with #checkPage", () => { - it("returns a list of green domains, when passed a page object", async () => { + describe("checking domains against a db snapshot", () => { + it("returns a list of green domains, when passed a database array", async () => { const db = await hosting.loadJSON(jsonPath); const greenDomains = await hosting.check( ["www.thegreenwebfoundation.org", "fonts.googleapis.com"], @@ -34,13 +33,26 @@ describe("hosting", () => { ); expect(greenDomains).toHaveLength(2); - const expectedGreendomains = [ - "www.thegreenwebfoundation.org", - "fonts.googleapis.com", - ]; - greenDomains.forEach((dom) => { - expect(expectedGreendomains).toContain(dom); - }); + }); + it("returns a list of green domains, when pass a database array via options", async () => { + const db = await hosting.loadJSON(jsonPath); + const greenDomains = await hosting.check( + ["www.thegreenwebfoundation.org", "fonts.googleapis.com"], + { db } + ); + + expect(greenDomains).toHaveLength(2); + }); + it("fails if verbose=true is set", async () => { + const db = await hosting.loadJSON(jsonPath); + await expect(() => { + hosting.check( + ["www.thegreenwebfoundation.org", "fonts.googleapis.com"], + { verbose: true, db } + ); + }).toThrowError( + "verbose mode cannot be used with a local lookup database" + ); }); }); describe("checking a single domain with #check", () => { @@ -48,7 +60,31 @@ 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", { + 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", { + userAgentIdentifier: requestHeaderComment, + }); + expect(httpsGetSpy).toHaveBeenCalledTimes(1); + expect(httpsGetSpy).toHaveBeenLastCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { "User-Agent": "co2js/1.2.34 TestRunner" }, + }), + expect.any(Function) + ); + }); + it("sets the correct user agent header when passed as a parameter", async () => { await hosting.check("google.com", null, requestHeaderComment); expect(httpsGetSpy).toHaveBeenCalledTimes(1); expect(httpsGetSpy).toHaveBeenLastCalledWith( @@ -65,5 +101,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"], { + 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, + }), + }); + }); }); });