From e1e4f6fa8feb4fd69307c22317024c8cfa312482 Mon Sep 17 00:00:00 2001 From: paraschi Date: Wed, 7 Aug 2024 22:48:43 +0300 Subject: [PATCH] feat: adding tests --- .nycrc.json | 8 +- src/canonical/handler.js | 35 +++------ test/audits/canonical.test.js | 138 ++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 27 deletions(-) diff --git a/.nycrc.json b/.nycrc.json index 4e4ce806..041f08c1 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -3,10 +3,10 @@ "lcov", "text" ], - "check-coverage": false, - "lines": 100, - "branches": 100, - "statements": 100, + "check-coverage": true, + "lines": 90, + "branches": 90, + "statements": 90, "all": true, "include": [ "src/**/*.js" diff --git a/src/canonical/handler.js b/src/canonical/handler.js index 53317d02..7bd45786 100644 --- a/src/canonical/handler.js +++ b/src/canonical/handler.js @@ -100,7 +100,7 @@ const CANONICAL_CHECKS = Object.freeze({ * @param {Object} context.log - The logging object to log information. * @returns {Promise>} A promise that resolves to an array of top pages. */ -async function getTopPagesForSiteId(dataAccess, siteId, context, log) { +export async function getTopPagesForSiteId(dataAccess, siteId, context, log) { try { const result = await dataAccess.getTopPagesForSite(siteId, 'ahrefs', 'global'); log.info('Received top pages response:', JSON.stringify(result, null, 2)); @@ -127,7 +127,7 @@ async function getTopPagesForSiteId(dataAccess, siteId, context, log) { * @param {Object} log - The logging object to log information. * @returns {Promise} An object containing the canonical URL and an array of checks. */ -async function validateCanonicalTag(url, log) { +export async function validateCanonicalTag(url, log) { // in case of undefined or null URL in the 200 top pages list if (!url) { const errorMessage = 'URL is undefined or null'; @@ -146,8 +146,6 @@ async function validateCanonicalTag(url, log) { log.info(`Fetching URL: ${url}`); const response = await fetch(url); const html = await response.text(); - log.info(`Fetched HTML content for URL: ${url}`); - const dom = new JSDOM(html); const { head } = dom.window.document; const canonicalLinks = head.querySelectorAll('link[rel="canonical"]'); @@ -183,8 +181,6 @@ async function validateCanonicalTag(url, log) { log.info(`Multiple canonical tags found for URL: ${url}`); } else if (canonicalLinks.length === 1) { const canonicalLink = canonicalLinks[0]; - log.info(`Canonical link element: ${JSON.stringify(canonicalLink.outerHTML)}`); - const href = canonicalLink.getAttribute('href'); if (!href) { checks.push({ @@ -200,21 +196,20 @@ async function validateCanonicalTag(url, log) { check: CANONICAL_CHECKS.CANONICAL_TAG_NONEMPTY.check, success: true, }); - log.info(`Valid canonical URL resolved: ${canonicalUrl}`); // Check if canonical URL points to itself if (canonicalUrl === url) { checks.push({ check: CANONICAL_CHECKS.CANONICAL_SELF_REFERENCED.check, success: true, }); - log.info(`Canonical URL correctly references itself: ${canonicalUrl}`); + log.info(`Canonical URL ${canonicalUrl} references itself`); } else { checks.push({ check: CANONICAL_CHECKS.CANONICAL_SELF_REFERENCED.check, success: false, explanation: CANONICAL_CHECKS.CANONICAL_SELF_REFERENCED.explanation, }); - log.info(`Canonical URL does not reference itself: ${canonicalUrl}`); + log.info(`Canonical URL ${canonicalUrl} does not reference itself`); } } catch (error) { checks.push({ @@ -239,11 +234,10 @@ async function validateCanonicalTag(url, log) { check: CANONICAL_CHECKS.CANONICAL_TAG_IN_HEAD.check, success: true, }); - log.info(`Canonical tag is correctly placed in the head section for URL: ${url}`); } } - log.info(`Validation checks for URL: ${url}, Checks: ${JSON.stringify(checks)}`); + log.info(`Checks: ${JSON.stringify(checks)}`); return { canonicalUrl, checks }; } catch (error) { const errorMessage = `Error validating canonical tag for ${url}: ${error.message}`; @@ -265,10 +259,9 @@ async function validateCanonicalTag(url, log) { * @param {string} canonicalUrl - The canonical URL to validate. * @param {string} baseUrl - The base URL to compare against. * @param log - * @returns {Array} Array of check results, each with a check and error if the check failed. + * @returns {Array} Array of check results. */ - -function validateCanonicalUrlFormat(canonicalUrl, baseUrl, log) { +export function validateCanonicalFormat(canonicalUrl, baseUrl, log) { const checks = []; let url; let base; @@ -313,7 +306,6 @@ function validateCanonicalUrlFormat(canonicalUrl, baseUrl, log) { check: CANONICAL_CHECKS.CANONICAL_URL_ABSOLUTE.check, success: true, }); - log.info(`Canonical URL is absolute: ${canonicalUrl}`); } // Check if the canonical URL has the same protocol as the base URL @@ -329,7 +321,6 @@ function validateCanonicalUrlFormat(canonicalUrl, baseUrl, log) { check: CANONICAL_CHECKS.CANONICAL_URL_SAME_PROTOCOL.check, success: true, }); - log.info('Canonical URL uses the same protocol'); } // Check if the canonical URL has the same domain as the base URL @@ -345,7 +336,6 @@ function validateCanonicalUrlFormat(canonicalUrl, baseUrl, log) { check: CANONICAL_CHECKS.CANONICAL_URL_SAME_DOMAIN.check, success: true, }); - log.info(`Canonical URL ${canonicalUrl} has the same domain as base URL: ${canonicalUrl}`); } // Check if the canonical URL is in lowercase @@ -361,7 +351,6 @@ function validateCanonicalUrlFormat(canonicalUrl, baseUrl, log) { check: CANONICAL_CHECKS.CANONICAL_URL_LOWERCASED.check, success: true, }); - log.info(`Canonical URL is lowercased: ${canonicalUrl}`); } return checks; @@ -375,7 +364,7 @@ function validateCanonicalUrlFormat(canonicalUrl, baseUrl, log) { * @param {Set} [visitedUrls=new Set()] - A set of visited URLs to detect redirect loops. * @returns {Promise} An object with the check result and any error if the check failed. */ -async function validateCanonicalUrlContentsRecursive(canonicalUrl, log, visitedUrls = new Set()) { +export async function validateCanonicalRecursively(canonicalUrl, log, visitedUrls = new Set()) { const checks = []; // Check for redirect loops @@ -407,7 +396,7 @@ async function validateCanonicalUrlContentsRecursive(canonicalUrl, log, visitedU // Check for redirection to another URL if (canonicalUrl !== finalUrl) { log.info(`Canonical URL redirects to: ${finalUrl}`); - const result = await validateCanonicalUrlContentsRecursive(finalUrl, log, visitedUrls); + const result = await validateCanonicalRecursively(finalUrl, log, visitedUrls); checks.push(...result.checks); } else { checks.push({ @@ -496,11 +485,11 @@ export async function canonicalAuditRunner(baseURL, context, site) { if (canonicalUrl) { log.info(`Found Canonical URL: ${canonicalUrl}`); - const urlFormatChecks = validateCanonicalUrlFormat(canonicalUrl, baseURL, log); + const urlFormatChecks = validateCanonicalFormat(canonicalUrl, baseURL, log); log.info(`Canonical URL format results for ${canonicalUrl}: ${JSON.stringify(urlFormatChecks)}`); checks.push(...urlFormatChecks); - const urlContentCheck = await validateCanonicalUrlContentsRecursive(canonicalUrl, log); + const urlContentCheck = await validateCanonicalRecursively(canonicalUrl, log); log.info(`Canonical URL recursive result for ${canonicalUrl}: ${JSON.stringify(urlContentCheck)}`); checks.push(...urlContentCheck); } @@ -523,7 +512,7 @@ export async function canonicalAuditRunner(baseURL, context, site) { return acc; }, {}); - log.info(`Successfully completed canonical audit for site: ${baseURL}`); + log.info(`Successfully completed Canonical Audit for site: ${baseURL}`); log.info(`Audit results: ${JSON.stringify(aggregatedResults)}`); return { diff --git a/test/audits/canonical.test.js b/test/audits/canonical.test.js index de87e432..7c0d0b6a 100644 --- a/test/audits/canonical.test.js +++ b/test/audits/canonical.test.js @@ -9,3 +9,141 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ + +/* eslint-env mocha */ + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import nock from 'nock'; +import { + getTopPagesForSiteId, validateCanonicalTag, validateCanonicalFormat, + validateCanonicalRecursively, canonicalAuditRunner, +} from '../../src/canonical/handler.js'; // Adjust the import path as necessary + +chai.use(sinonChai); +chai.use(chaiAsPromised); +const { expect } = chai; + +describe('Canonical URL Tests', () => { + let log; + + beforeEach(() => { + log = { + info: sinon.stub(), + error: sinon.stub(), + }; + }); + + afterEach(() => { + sinon.restore(); + nock.cleanAll(); + }); + + describe('getTopPagesForSiteId', () => { + it('should return top pages for a given site ID', async () => { + const dataAccess = { + getTopPagesForSite: sinon.stub().resolves([{ getURL: () => 'http://example.com/page1' }]), + }; + const siteId = 'testSiteId'; + const context = { log }; + + const result = await getTopPagesForSiteId(dataAccess, siteId, context, log); + + expect(result).to.deep.equal([{ url: 'http://example.com/page1' }]); + expect(log.info).to.have.been.called; + }); + + it('should handle error and return an empty array', async () => { + const dataAccess = { + getTopPagesForSite: sinon.stub().rejects(new Error('Test error')), + }; + const siteId = 'testSiteId'; + const context = { log }; + + const result = await getTopPagesForSiteId(dataAccess, siteId, context, log); + + expect(result).to.deep.equal([]); + expect(log.error).to.have.been.calledWith('Error retrieving top pages for site testSiteId: Test error'); + }); + }); + + describe('validateCanonicalTag', () => { + it('should handle missing canonical tag', async () => { + const url = 'http://example.com'; + const html = ''; + nock('http://example.com').get('/').reply(200, html); + + const result = await validateCanonicalTag(url, log); + + expect(result.canonicalUrl).to.be.null; + expect(result.checks).to.deep.include({ + check: 'canonical-tag-exists', + success: false, + explanation: 'The canonical tag is missing, which can lead to duplicate content issues and negatively affect SEO rankings.', + }); + expect(log.info).to.have.been.called; + }); + + it('should handle fetch error', async () => { + const url = 'http://example.com'; + nock('http://example.com').get('/').replyWithError('Test error'); + + const result = await validateCanonicalTag(url, log); + + expect(result.canonicalUrl).to.be.null; + expect(result.checks).to.deep.include({ check: 'canonical-url-fetch-error', success: false, explanation: 'There was an error fetching the canonical URL, which prevents validation of the canonical tag.' }); + // expect(log.error).to.have.been.calledWith('Error validating canonical tag for http://example.com: request to http://example.com/ failed, reason: Test error'); + }); + }); + + describe('validateCanonicalUrlFormat', () => { + it('should validate canonical URL format successfully', () => { + const canonicalUrl = 'http://example.com/page'; + const baseUrl = 'http://example.com'; + + const result = validateCanonicalFormat(canonicalUrl, baseUrl, log); + + expect(result).to.deep.include({ check: 'canonical-url-absolute', success: true }); + expect(result).to.deep.include({ check: 'canonical-url-same-protocol', success: true }); + expect(result).to.deep.include({ check: 'canonical-url-same-domain', success: true }); + expect(result).to.deep.include({ check: 'canonical-url-lowercased', success: true }); + }); + + it('should handle invalid canonical URL', () => { + const canonicalUrl = 'invalid-url'; + const baseUrl = 'http://example.com'; + + const result = validateCanonicalFormat(canonicalUrl, baseUrl, log); + + expect(result).to.deep.include({ check: 'url-defined', success: false, explanation: 'The URL is undefined or null, which prevents the canonical tag validation process.' }); + expect(log.error).to.have.been.calledWith('Invalid URL: invalid-url'); + }); + }); + + describe('validateCanonicalUrlContentsRecursive', () => { + it('should validate canonical URL contents successfully', async () => { + const canonicalUrl = 'http://example.com/page'; + nock('http://example.com').get('/page').reply(200); + + const result = await validateCanonicalRecursively(canonicalUrl, log); + + expect(result).to.deep.include({ check: 'canonical-url-status-ok', success: true }); + expect(result).to.deep.include({ check: 'canonical-url-no-redirect', success: true }); + }); + }); + + describe('canonicalAuditRunner', () => { + it('should run canonical audit successfully', async () => { + const baseURL = 'http://example.com'; + const context = { log, dataAccess: { getTopPagesForSite: sinon.stub().resolves([{ getURL: () => 'http://example.com/page1' }]) } }; + const site = { getId: () => 'testSiteId' }; + + const result = await canonicalAuditRunner(baseURL, context, site); + + expect(result).to.be.an('object'); + expect(log.info).to.have.been.called; + }); + }); +});