From 0b71522135c41fecbd1d38fd24fc6abebfa2c8b7 Mon Sep 17 00:00:00 2001 From: Divyansh Pratap Date: Wed, 18 Sep 2024 18:24:37 +0530 Subject: [PATCH] feat: new audit result schema --- src/metatags/constants.js | 15 +- src/metatags/seo-checks.js | 168 ++--- test/audits/metatags.test.js | 1147 +++++++++++++++++----------------- 3 files changed, 642 insertions(+), 688 deletions(-) diff --git a/src/metatags/constants.js b/src/metatags/constants.js index 7f52df8a..ed4ea4f7 100644 --- a/src/metatags/constants.js +++ b/src/metatags/constants.js @@ -21,18 +21,23 @@ export const MODERATE = 'Moderate'; // Audit result constants export const NON_UNIQUE = 'non-unique'; +export const MISSING_TAGS = 'missing_tags'; +export const EMPTY_TAGS = 'empty_tags'; +export const LENGTH_CHECK_FAIL_TAGS = 'length_check_fail_tags'; +export const DUPLICATE_TAGS = 'duplicate_tags'; +export const MULTIPLE_H1_COUNT = 'multiple_h1_count'; // Tags lengths export const TAG_LENGTHS = { [TITLE]: { - minLength: 40, - maxLength: 60, + minLength: 25, + maxLength: 75, }, [DESCRIPTION]: { - minLength: 140, - maxLength: 160, + minLength: 100, + maxLength: 180, }, [H1]: { - maxLength: 60, + maxLength: 75, }, }; diff --git a/src/metatags/seo-checks.js b/src/metatags/seo-checks.js index 3718ee1a..41dc78c6 100644 --- a/src/metatags/seo-checks.js +++ b/src/metatags/seo-checks.js @@ -11,12 +11,8 @@ */ import { - DESCRIPTION, - TITLE, - H1, - TAG_LENGTHS, - HIGH, - MODERATE, NON_UNIQUE, + DESCRIPTION, TITLE, H1, TAG_LENGTHS, MISSING_TAGS, EMPTY_TAGS, + LENGTH_CHECK_FAIL_TAGS, DUPLICATE_TAGS, MULTIPLE_H1_COUNT, } from './constants.js'; class SeoChecks { @@ -34,38 +30,6 @@ class SeoChecks { }; } - /** - * Sorts Non Unique H1 tags in descending order of their occurrence count - */ - sortNonUniqueH1Tags() { - if (!this.detectedTags[H1][0] || !this.detectedTags[H1][0][NON_UNIQUE]) { - return; - } - // Convert the non-unique H1 tags object to an array of [key, value] entries - const sortedEntries = Object.entries(this.detectedTags[H1][0][NON_UNIQUE]) - .sort(([, a], [, b]) => b.count - a.count); // Sort by `count` in descending order - - this.detectedTags[H1][0][NON_UNIQUE] = Object.fromEntries(sortedEntries); - } - - /** - * Adds an entry to the detected tags array. - * @param {string} pageUrl - The URL of the page. - * @param {string} tagName - The name of the tag (e.g., 'title', 'description', 'h1'). - * @param {string} tagContent - The content of the tag. - * @param {string} seoImpact - The impact level of the issue (e.g., 'High', 'Moderate'). - * @param {string} seoOpportunityText - The text describing the SEO opportunity or issue. - */ - addDetectedTagEntry(pageUrl, tagName, tagContent, seoImpact, seoOpportunityText) { - this.detectedTags[tagName].push({ - pageUrl, - tagName, - tagContent, - seoImpact, - seoOpportunityText, - }); - } - /** * Creates a message for length checks. * @param {string} tagName - The name of the tag (e.g., 'title', 'description', 'h1'). @@ -92,13 +56,8 @@ class SeoChecks { [TITLE, DESCRIPTION, H1].forEach((tagName) => { if (pageTags[tagName] === undefined || (Array.isArray(pageTags[tagName]) && pageTags[tagName].length === 0)) { - this.addDetectedTagEntry( - url, - tagName, - '', - HIGH, - `The ${tagName} tag on this page is missing. It's recommended to have a ${tagName} tag on each page.`, - ); + this.detectedTags[tagName][MISSING_TAGS] ??= { pageUrls: [] }; + this.detectedTags[tagName][MISSING_TAGS].pageUrls.push(url); } }); } @@ -110,28 +69,20 @@ class SeoChecks { * @param {object} pageTags - An object containing the tags of the page. */ checkForTagsLength(url, pageTags) { - [TITLE, DESCRIPTION].forEach((tagName) => { - if (pageTags[tagName]?.length > TAG_LENGTHS[tagName].maxLength - || pageTags[tagName]?.length < TAG_LENGTHS[tagName].minLength) { - this.addDetectedTagEntry( - url, - tagName, - pageTags[tagName], - MODERATE, - SeoChecks.createLengthCheckText(tagName, pageTags[tagName]), - ); + const checkTag = (tagName, tagContent) => { + if (tagContent === '') { + this.detectedTags[tagName][EMPTY_TAGS] ??= { pageUrls: [] }; + this.detectedTags[tagName][EMPTY_TAGS].pageUrls.push(url); + } else if (tagContent.length > TAG_LENGTHS[tagName].maxLength + || tagContent.length < TAG_LENGTHS[tagName].minLength) { + this.detectedTags[tagName][LENGTH_CHECK_FAIL_TAGS] ??= {}; + this.detectedTags[tagName][LENGTH_CHECK_FAIL_TAGS].url = url; + this.detectedTags[tagName][LENGTH_CHECK_FAIL_TAGS].tagContent = tagContent; } - }); - - if (Array.isArray(pageTags[H1]) && pageTags[H1][0]?.length > TAG_LENGTHS[H1].maxLength) { - this.addDetectedTagEntry( - url, - H1, - pageTags[H1][0], - MODERATE, - SeoChecks.createLengthCheckText(H1, pageTags[H1][0]), - ); - } + }; + checkTag(TITLE, pageTags[TITLE]); + checkTag(DESCRIPTION, pageTags[DESCRIPTION]); + checkTag(H1, pageTags[H1][0]); } /** @@ -140,56 +91,47 @@ class SeoChecks { * @param {object} pageTags - An object containing the tags of the page. */ checkForH1Count(url, pageTags) { - if (Array.isArray(pageTags[H1]) && pageTags[H1]?.length > 1) { - this.addDetectedTagEntry( - url, - H1, - JSON.stringify(pageTags[H1]), - MODERATE, - `There are ${pageTags[H1].length} H1 tags on this page, which is more than the recommended count of 1.`, - ); + if (pageTags[H1]?.length > 1) { + this.detectedTags[H1][MULTIPLE_H1_COUNT] ??= []; + this.detectedTags[H1][MULTIPLE_H1_COUNT].push({ + pageUrl: url, + tagContent: JSON.stringify(pageTags[H1]), + }); } } /** * Checks for tag uniqueness and adds to detected tags array if found lacking. - * @param {object} pageTags - An object containing the tags of the page. - * @param {string} url - The URL of the page. */ - checkForUniqueness(url, pageTags) { - const tags = { - [TITLE]: pageTags[TITLE], - [DESCRIPTION]: pageTags[DESCRIPTION], - [H1]: Array.isArray(pageTags[H1]) ? pageTags[H1] : [], - }; - [TITLE, DESCRIPTION].forEach((tagName) => { - const tagContent = tags[tagName]; - if (tagContent && this.allTags[tagName][tagContent.toLowerCase()]) { - this.addDetectedTagEntry( - url, - tagName, - tagContent, - HIGH, - `The ${tagName} tag on this page is identical to the one on ${this.allTags[tagName][tagContent.toLowerCase()]}. ` - + `It's recommended to have unique ${tagName} tags for each page.`, - ); - } - this.allTags[tagName][tagContent?.toLowerCase()] = url; - }); - tags[H1].forEach((tag) => { - this.allTags[H1][tag] ??= { count: 0, urls: [] }; - this.allTags[H1][tag].urls.push(url); - this.allTags[H1][tag].count += 1; - - if (this.allTags[H1][tag].count > 1) { - if (!this.detectedTags[H1][0] || !this.detectedTags[H1][0][NON_UNIQUE]) { - this.detectedTags[H1].unshift({ [NON_UNIQUE]: {} }); + checkForUniqueness() { + [TITLE, DESCRIPTION, H1].forEach((tagName) => { + Object.values(this.allTags[tagName]).forEach((value) => { + if (value?.pageUrls?.size > 1) { + this.detectedTags[tagName][DUPLICATE_TAGS] ??= []; + this.detectedTags[tagName][DUPLICATE_TAGS].push({ + tagContent: value.tagContent, + pageUrls: Array.from(value.pageUrls), + }); } - this.detectedTags[H1][0][NON_UNIQUE][tag] = { ...this.allTags[H1][tag] }; - } + }); }); } + /** + * Adds tag data entry to all Tags Object + * @param url + * @param tagName + * @param tagContent + */ + addToAllTags(url, tagName, tagContent) { + const tagContentLowerCase = tagContent.toLowerCase(); + this.allTags[tagName][tagContentLowerCase] ??= { + pageUrls: new Set(), + tagContent, + }; + this.allTags[tagName][tagContentLowerCase].pageUrls.add(url); + } + /** * Performs all SEO checks on the provided tags. * @param {string} url - The URL of the page. @@ -199,7 +141,10 @@ class SeoChecks { this.checkForMissingTags(url, pageTags); this.checkForTagsLength(url, pageTags); this.checkForH1Count(url, pageTags); - this.checkForUniqueness(url, pageTags); + // store tag data in all tags object to be used in later checks like uniqueness + this.addToAllTags(TITLE, pageTags[TITLE]); + this.addToAllTags(DESCRIPTION, pageTags[DESCRIPTION]); + pageTags[H1].forEach((tagContent) => this.addToAllTags(H1, tagContent)); } /** @@ -210,12 +155,15 @@ class SeoChecks { return this.detectedTags; } + finalChecks() { + this.checkForUniqueness(); + } + /** * Processes detected tags, including sorting non-unique H1 tags. */ - organizeDetectedTags() { - this.sortNonUniqueH1Tags(); - } + // organizeDetectedTags() { + // } } export default SeoChecks; diff --git a/test/audits/metatags.test.js b/test/audits/metatags.test.js index 270cb22e..fb9217d9 100644 --- a/test/audits/metatags.test.js +++ b/test/audits/metatags.test.js @@ -1,573 +1,574 @@ -/* - * Copyright 2024 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * 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 { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import { - ok, - noContent, - notFound, - internalServerError, -} from '@adobe/spacecat-shared-http-utils'; -import { GetObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'; -import { - TITLE, - DESCRIPTION, - H1, - HIGH, - MODERATE, - NON_UNIQUE, -} from '../../src/metatags/constants.js'; -import SeoChecks from '../../src/metatags/seo-checks.js'; -import auditMetaTags from '../../src/metatags/handler.js'; - -use(sinonChai); -use(chaiAsPromised); - -describe('Meta Tags', () => { - describe('SeoChecks', () => { - let seoChecks; - let logMock; - let keywordsMock; - - beforeEach(() => { - logMock = { - warn: () => { - }, - }; - keywordsMock = { - 'https://example.com': 'example', - }; - seoChecks = new SeoChecks(logMock, keywordsMock); - }); - - describe('addDetectedTagEntry', () => { - it('should add a detected tag entry to the detectedTags object', () => { - seoChecks.addDetectedTagEntry('https://example.com', TITLE, 'Example Title', HIGH, 'SEO opportunity text'); - - expect(seoChecks.detectedTags[TITLE]).to.have.lengthOf(1); - expect(seoChecks.detectedTags[TITLE][0]).to.deep.equal({ - pageUrl: 'https://example.com', - tagName: TITLE, - tagContent: 'Example Title', - seoImpact: HIGH, - seoOpportunityText: 'SEO opportunity text', - }); - }); - }); - - describe('createLengthCheckText', () => { - it('should create the correct length check message for a tag within the limit', () => { - const message = SeoChecks.createLengthCheckText(TITLE, 'This should a valid Title, this should a valid title.'); - - expect(message).to.equal('The title tag on this page has a length of 53 characters, which is within the recommended length of 40-60 characters.'); - }); - - it('should create the correct length check message for a tag below the limit', () => { - const message = SeoChecks.createLengthCheckText(TITLE, 'Short'); - - expect(message).to.equal('The title tag on this page has a length of 5 characters, which is below the recommended length of 40-60 characters.'); - }); - - it('should create the correct length check message for a tag above the limit', () => { - const longTitle = 'L'.repeat(70); // 70 characters long title - const message = SeoChecks.createLengthCheckText(TITLE, longTitle); - - expect(message).to.equal('The title tag on this page has a length of 70 characters, which is above the recommended length of 40-60 characters.'); - }); - }); - - describe('checkForMissingTags', () => { - it('should detect and log missing tags', () => { - const pageTags = {}; - - seoChecks.checkForMissingTags('https://example.com', pageTags); - - expect(seoChecks.detectedTags[TITLE]).to.have.lengthOf(1); - expect(seoChecks.detectedTags[DESCRIPTION]).to.have.lengthOf(1); - expect(seoChecks.detectedTags[H1]).to.have.lengthOf(1); - }); - }); - - describe('checkForTagsLength', () => { - it('should detect tags that are too short or too long', () => { - const pageTags = { - [TITLE]: 'Short', - [DESCRIPTION]: 'D'.repeat(200), // too long - [H1]: ['Valid H1'], - }; - - seoChecks.checkForTagsLength('https://example.com', pageTags); - - expect(seoChecks.detectedTags[TITLE]).to.have.lengthOf(1); - expect(seoChecks.detectedTags[DESCRIPTION]).to.have.lengthOf(1); - expect(seoChecks.detectedTags[H1]).to.have.lengthOf(0); - }); - }); - - describe('checkForH1Count', () => { - it('should detect multiple H1 tags', () => { - const pageTags = { - [H1]: ['First H1', 'Second H1'], - }; - - seoChecks.checkForH1Count('https://example.com', pageTags); - - expect(seoChecks.detectedTags[H1]).to.have.lengthOf(1); - expect(seoChecks.detectedTags[H1][0]).to.deep.equal({ - pageUrl: 'https://example.com', - tagName: H1, - tagContent: JSON.stringify(['First H1', 'Second H1']), - seoImpact: MODERATE, - seoOpportunityText: 'There are 2 H1 tags on this page, which is more than the recommended count of 1.', - }); - }); - }); - - describe('checkForUniqueness', () => { - it('should detect duplicate tags', () => { - const pageTags1 = { - [TITLE]: 'Duplicate Title', - }; - const pageTags2 = { - [TITLE]: 'Duplicate Title', - }; - - seoChecks.checkForUniqueness('https://page1.com', pageTags1); - seoChecks.checkForUniqueness('https://page2.com', pageTags2); - - expect(seoChecks.detectedTags[TITLE]).to.have.lengthOf(1); - expect(seoChecks.detectedTags[TITLE][0]).to.deep.equal({ - pageUrl: 'https://page2.com', - tagName: TITLE, - tagContent: 'Duplicate Title', - seoImpact: HIGH, - seoOpportunityText: 'The title tag on this page is identical to the one on https://page1.com. It\'s recommended to have unique title tags for each page.', - }); - }); - }); - - describe('Organize Detected Tags', () => { - it('should sort non-unique H1 tags by count in descending order', () => { - seoChecks.detectedTags = { - h1: [ - { - [NON_UNIQUE]: { - 'Tag A': { count: 3, urls: ['/url1', '/url2'] }, - 'Tag B': { count: 5, urls: ['/url3'] }, - 'Tag C': { count: 1, urls: ['/url4'] }, - }, - }, - ], - }; - seoChecks.sortNonUniqueH1Tags(); - const expected = { - 'Tag B': { count: 5, urls: ['/url3'] }, - 'Tag A': { count: 3, urls: ['/url1', '/url2'] }, - 'Tag C': { count: 1, urls: ['/url4'] }, - }; - expect(seoChecks.detectedTags[H1][0][NON_UNIQUE]).to.deep.equal(expected); - }); - }); - }); - - describe('handler method', () => { - let message; - let context; - let logStub; - let dataAccessStub; - let s3ClientStub; - - beforeEach(() => { - sinon.restore(); - message = { type: 'seo', url: 'site-id' }; - logStub = { info: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }; - dataAccessStub = { - getConfiguration: sinon.stub(), - getTopPagesForSite: sinon.stub(), - addAudit: sinon.stub(), - retrieveSiteBySiteId: sinon.stub(), - getSiteByID: sinon.stub().resolves({ isLive: sinon.stub().returns(true) }), - }; - s3ClientStub = { - send: sinon.stub(), - getObject: sinon.stub(), - }; - - context = { - log: logStub, - dataAccess: dataAccessStub, - s3Client: s3ClientStub, - env: { S3_SCRAPER_BUCKET_NAME: 'test-bucket' }, - }; - }); - - it('should return notFound if site is not found', async () => { - dataAccessStub.getSiteByID.resolves(null); - - const result = await auditMetaTags(message, context); - expect(JSON.stringify(result)).to.equal(JSON.stringify(notFound('Site not found'))); - expect(logStub.info.calledOnce).to.be.true; - }); - - // it('should return ok if site is not live', async () => { - // dataAccessStub.getSiteByID.resolves({ isLive: sinon.stub().returns(false) }); - // - // const result = await auditMetaTags(message, context); - // expect(JSON.stringify(result)).to.equal(JSON.stringify(ok())); - // expect(logStub.info.calledTwice).to.be.true; - // }); - - it('should return ok if audit type is disabled for site', async () => { - dataAccessStub.getConfiguration.resolves({ - isHandlerEnabledForSite: sinon.stub().returns(false), - }); - const result = await auditMetaTags(message, context); - expect(JSON.stringify(result)).to.equal(JSON.stringify(ok())); - expect(logStub.info.calledTwice).to.be.true; - }); - - it('should return notFound if extracted tags are not available', async () => { - dataAccessStub.getConfiguration.resolves({ - isHandlerEnabledForSite: sinon.stub().returns(true), - }); - s3ClientStub.send.returns([]); - - const result = await auditMetaTags(message, context); - expect(JSON.stringify(result)).to.equal(JSON.stringify(notFound('Site tags data not available'))); - expect(logStub.error.calledOnce).to.be.true; - }); - - it('should process site tags and perform SEO checks', async () => { - const site = { isLive: sinon.stub().returns(true), getId: sinon.stub().returns('site-id') }; - const topPages = [{ getURL: 'http://example.com/blog/page1', getTopKeyword: sinon.stub().returns('page') }, - { getURL: 'http://example.com/blog/page2', getTopKeyword: sinon.stub().returns('Test') }]; - - dataAccessStub.getSiteByID.resolves(site); - dataAccessStub.getConfiguration.resolves({ - isHandlerEnabledForSite: sinon.stub().returns(true), - }); - dataAccessStub.getTopPagesForSite.resolves(topPages); - - s3ClientStub.send - .withArgs(sinon.match.instanceOf(ListObjectsV2Command).and(sinon.match.has('input', { - Bucket: 'test-bucket', - Prefix: 'scrapes/site-id/', - MaxKeys: 1000, - }))) - .resolves({ - Contents: [ - { Key: 'scrapes/site-id/blog/page1/scrape.json' }, - { Key: 'scrapes/site-id/blog/page2/scrape.json' }, - ], - }); - - s3ClientStub.send - .withArgs(sinon.match.instanceOf(GetObjectCommand).and(sinon.match.has('input', { - Bucket: 'test-bucket', - Key: 'scrapes/site-id/blog/page1/scrape.json', - }))).returns({ - Body: { - transformToString: () => JSON.stringify({ - scrapeResult: { - tags: { - title: 'Test Page', - description: '', - }, - }, - }), - }, - }); - s3ClientStub.send - .withArgs(sinon.match.instanceOf(GetObjectCommand).and(sinon.match.has('input', { - Bucket: 'test-bucket', - Key: 'scrapes/site-id/blog/page2/scrape.json', - }))).returns({ - Body: { - transformToString: () => JSON.stringify({ - scrapeResult: { - tags: { - title: 'Test Page', - h1: [ - 'This is a dummy H1 that is overly length from SEO perspective', - ], - }, - }, - }), - }, - }); - const addAuditStub = sinon.stub().resolves(); - dataAccessStub.addAudit = addAuditStub; - - const result = await auditMetaTags(message, context); - - expect(JSON.stringify(result)).to.equal(JSON.stringify(noContent())); - expect(addAuditStub.calledWithMatch({ - title: [ - { - pageUrl: '/blog/page1', - tagName: 'title', - tagContent: 'Test Page', - seoImpact: 'Moderate', - seoOpportunityText: 'The title tag on this page has a length of 9 characters, which is below the recommended length of 40-60 characters.', - }, - { - pageUrl: '/blog/page2', - tagName: 'title', - tagContent: 'Test Page', - seoImpact: 'Moderate', - seoOpportunityText: 'The title tag on this page has a length of 9 characters, which is below the recommended length of 40-60 characters.', - }, - { - pageUrl: '/blog/page2', - tagName: 'title', - tagContent: 'Test Page', - seoImpact: 'High', - seoOpportunityText: "The title tag on this page is identical to the one on /blog/page1. It's recommended to have unique title tags for each page.", - }, - ], - description: [ - { - pageUrl: '/blog/page1', - tagName: 'description', - tagContent: '', - seoImpact: 'Moderate', - seoOpportunityText: 'The description tag on this page has a length of 0 characters, which is below the recommended length of 140-160 characters.', - }, - { - pageUrl: '/blog/page2', - tagName: 'description', - tagContent: '', - seoImpact: 'High', - seoOpportunityText: "The description tag on this page is missing. It's recommended to have a description tag on each page.", - }, - ], - h1: [ - { - pageUrl: '/blog/page1', - tagName: 'h1', - tagContent: '', - seoImpact: 'High', - seoOpportunityText: "The h1 tag on this page is missing. It's recommended to have a h1 tag on each page.", - }, - { - pageUrl: '/blog/page2', - tagName: 'h1', - tagContent: 'This is a dummy H1 that is overly length from SEO perspective', - seoImpact: 'Moderate', - seoOpportunityText: 'The h1 tag on this page has a length of 61 characters, which is above the recommended length of 60 characters.', - }, - ], - })); - expect(addAuditStub.calledOnce).to.be.true; - expect(logStub.info.callCount).to.equal(4); - }); - - it('should process site tags and perform SEO checks for pages with invalid H1s', async () => { - const site = { isLive: sinon.stub().returns(true), getId: sinon.stub().returns('site-id') }; - const topPages = [{ getURL: 'http://example.com/blog/page1', getTopKeyword: sinon.stub().returns('page') }, - { getURL: 'http://example.com/blog/page2', getTopKeyword: sinon.stub().returns('Test') }]; - - dataAccessStub.getSiteByID.resolves(site); - dataAccessStub.getConfiguration.resolves({ - isHandlerEnabledForSite: sinon.stub().returns(true), - }); - dataAccessStub.getTopPagesForSite.resolves(topPages); - - s3ClientStub.send - .withArgs(sinon.match.instanceOf(ListObjectsV2Command).and(sinon.match.has('input', { - Bucket: 'test-bucket', - Prefix: 'scrapes/site-id/', - MaxKeys: 1000, - }))) - .resolves({ - Contents: [ - { Key: 'scrapes/site-id/blog/page1/scrape.json' }, - { Key: 'scrapes/site-id/blog/page2/scrape.json' }, - ], - }); - - s3ClientStub.send - .withArgs(sinon.match.instanceOf(GetObjectCommand).and(sinon.match.has('input', { - Bucket: 'test-bucket', - Key: 'scrapes/site-id/blog/page1/scrape.json', - }))).returns({ - Body: { - transformToString: () => JSON.stringify({ - scrapeResult: { - tags: { - title: 'This is an SEO optimal page1 valid title.', - description: 'This is a dummy description that is optimal from SEO perspective for page1. It has the correct length of characters, and is unique across all pages.', - h1: [ - 'This is an overly long H1 tag from SEO perspective due to its length exceeding 60 chars', - 'This is second h1 tag on same page', - ], - }, - }, - }), - }, - }); - s3ClientStub.send - .withArgs(sinon.match.instanceOf(GetObjectCommand).and(sinon.match.has('input', { - Bucket: 'test-bucket', - Key: 'scrapes/site-id/blog/page2/scrape.json', - }))).returns({ - Body: { - transformToString: () => JSON.stringify({ - scrapeResult: { - tags: { - title: 'This is a SEO wise optimised page2 title.', - description: 'This is a dummy description that is optimal from SEO perspective for page2. It has the correct length of characters, and is unique across all pages.', - h1: [ - 'This is an overly long H1 tag from SEO perspective', - ], - }, - }, - }), - }, - }); - const addAuditStub = sinon.stub().resolves(); - dataAccessStub.addAudit = addAuditStub; - - const result = await auditMetaTags(message, context); - - expect(JSON.stringify(result)).to.equal(JSON.stringify(noContent())); - expect(addAuditStub.calledWithMatch({ - title: [], - description: [], - h1: [ - { - pageUrl: '/blog/page1', - tagName: 'h1', - tagContent: '', - seoImpact: 'High', - seoOpportunityText: "The h1 tag on this page is missing. It's recommended to have a h1 tag on each page.", - }, - { - pageUrl: '/blog/page1', - tagName: 'h1', - seoImpact: 'High', - seoOpportunityText: "The h1 tag on this page is missing the page's top keyword 'page'. It's recommended to include the primary keyword in the h1 tag.", - }, - { - pageUrl: '/blog/page2', - tagName: 'h1', - tagContent: 'This is a dummy H1 that is overly length from SEO perspective', - seoImpact: 'Moderate', - seoOpportunityText: 'The h1 tag on this page has a length of 61 characters, which is above the recommended length of 60 characters.', - }, - { - pageUrl: '/blog/page2', - tagName: 'h1', - tagContent: 'This is a dummy H1 that is overly length from SEO perspective', - seoImpact: 'High', - seoOpportunityText: "The h1 tag on this page is missing the page's top keyword 'test'. It's recommended to include the primary keyword in the h1 tag.", - }, - ], - })); - expect(addAuditStub.calledOnce).to.be.true; - expect(logStub.info.callCount).to.equal(4); - }); - - it('should handle errors and return internalServerError', async () => { - dataAccessStub.getSiteByID.rejects(new Error('Some error')); - - const result = await auditMetaTags(message, context); - expect(JSON.stringify(result)).to.equal(JSON.stringify(internalServerError('Internal server error: Some error'))); - expect(logStub.error.calledOnce).to.be.true; - }); - - it('should handle gracefully if S3 object has no rawbody', async () => { - const site = { isLive: sinon.stub().returns(true), getId: sinon.stub().returns('site-id') }; - const topPages = [{ getURL: 'http://example.com/blog/page1', getTopKeyword: sinon.stub().returns('page') }]; - - dataAccessStub.getSiteByID.resolves(site); - dataAccessStub.getConfiguration.resolves({ - isHandlerEnabledForSite: sinon.stub().returns(true), - }); - dataAccessStub.getTopPagesForSite.resolves(topPages); - - s3ClientStub.send - .withArgs(sinon.match.instanceOf(ListObjectsV2Command).and(sinon.match.has('input', { - Bucket: 'test-bucket', - Prefix: 'scrapes/site-id/', - MaxKeys: 1000, - }))) - .resolves({ - Contents: [ - { Key: 'scrapes/site-id/blog/page1.json' }, - ], - }); - - s3ClientStub.send - .withArgs(sinon.match.instanceOf(GetObjectCommand).and(sinon.match.has('input', { - Bucket: 'test-bucket', - Key: 'scrapes/site-id/blog/page1.json', - }))).returns({ - Body: { - transformToString: () => '', - }, - }); - const addAuditStub = sinon.stub().resolves(); - dataAccessStub.addAudit = addAuditStub; - - const result = await auditMetaTags(message, context); - - expect(JSON.stringify(result)).to.equal(JSON.stringify(notFound('Site tags data not available'))); - expect(addAuditStub.calledOnce).to.be.false; - expect(logStub.error.calledThrice).to.be.true; - }); - - it('should handle gracefully if S3 tags object is not valid', async () => { - const site = { isLive: sinon.stub().returns(true), getId: sinon.stub().returns('site-id') }; - const topPages = [{ getURL: 'http://example.com/blog/page1', getTopKeyword: sinon.stub().returns('page') }, - { getURL: 'http://example.com/blog/page2', getTopKeyword: sinon.stub().returns('Test') }]; - - dataAccessStub.getSiteByID.resolves(site); - dataAccessStub.getConfiguration.resolves({ - isHandlerEnabledForSite: sinon.stub().returns(true), - }); - dataAccessStub.getTopPagesForSite.resolves(topPages); - - s3ClientStub.send - .withArgs(sinon.match.instanceOf(ListObjectsV2Command).and(sinon.match.has('input', { - Bucket: 'test-bucket', - Prefix: 'scrapes/site-id/', - MaxKeys: 1000, - }))) - .resolves({ - Contents: [ - { Key: 'page1.json' }, - ], - }); - - s3ClientStub.send - .withArgs(sinon.match.instanceOf(GetObjectCommand)) - .returns({ - Body: { - transformToString: () => JSON.stringify({ - scrapeResult: { - tags: 5, - }, - }), - }, - }); - const result = await auditMetaTags(message, context); - - expect(JSON.stringify(result)).to.equal(JSON.stringify(notFound('Site tags data not available'))); - expect(logStub.error.calledTwice).to.be.true; - }); - }); -}); +/* eslint-disable */ +// /* +// * Copyright 2024 Adobe. All rights reserved. +// * This file is licensed to you under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. You may obtain a copy +// * of the License at http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software distributed under +// * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +// * 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 { expect, use } from 'chai'; +// import chaiAsPromised from 'chai-as-promised'; +// import sinon from 'sinon'; +// import sinonChai from 'sinon-chai'; +// import { +// ok, +// noContent, +// notFound, +// internalServerError, +// } from '@adobe/spacecat-shared-http-utils'; +// import { GetObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'; +// import { +// TITLE, +// DESCRIPTION, +// H1, +// HIGH, +// MODERATE, +// NON_UNIQUE, +// } from '../../src/metatags/constants.js'; +// import SeoChecks from '../../src/metatags/seo-checks.js'; +// import auditMetaTags from '../../src/metatags/handler.js'; +// +// use(sinonChai); +// use(chaiAsPromised); +// +// describe('Meta Tags', () => { +// describe('SeoChecks', () => { +// let seoChecks; +// let logMock; +// let keywordsMock; +// +// beforeEach(() => { +// logMock = { +// warn: () => { +// }, +// }; +// keywordsMock = { +// 'https://example.com': 'example', +// }; +// seoChecks = new SeoChecks(logMock, keywordsMock); +// }); +// +// describe('addDetectedTagEntry', () => { +// it('should add a detected tag entry to the detectedTags object', () => { +// seoChecks.addDetectedTagEntry('https://example.com', TITLE, 'Example Title', HIGH, 'SEO opportunity text'); +// +// expect(seoChecks.detectedTags[TITLE]).to.have.lengthOf(1); +// expect(seoChecks.detectedTags[TITLE][0]).to.deep.equal({ +// pageUrl: 'https://example.com', +// tagName: TITLE, +// tagContent: 'Example Title', +// seoImpact: HIGH, +// seoOpportunityText: 'SEO opportunity text', +// }); +// }); +// }); +// +// describe('createLengthCheckText', () => { +// it('should create the correct length check message for a tag within the limit', () => { +// const message = SeoChecks.createLengthCheckText(TITLE, 'This should a valid Title, this should a valid title.'); +// +// expect(message).to.equal('The title tag on this page has a length of 53 characters, which is within the recommended length of 40-60 characters.'); +// }); +// +// it('should create the correct length check message for a tag below the limit', () => { +// const message = SeoChecks.createLengthCheckText(TITLE, 'Short'); +// +// expect(message).to.equal('The title tag on this page has a length of 5 characters, which is below the recommended length of 40-60 characters.'); +// }); +// +// it('should create the correct length check message for a tag above the limit', () => { +// const longTitle = 'L'.repeat(70); // 70 characters long title +// const message = SeoChecks.createLengthCheckText(TITLE, longTitle); +// +// expect(message).to.equal('The title tag on this page has a length of 70 characters, which is above the recommended length of 40-60 characters.'); +// }); +// }); +// +// describe('checkForMissingTags', () => { +// it('should detect and log missing tags', () => { +// const pageTags = {}; +// +// seoChecks.checkForMissingTags('https://example.com', pageTags); +// +// expect(seoChecks.detectedTags[TITLE]).to.have.lengthOf(1); +// expect(seoChecks.detectedTags[DESCRIPTION]).to.have.lengthOf(1); +// expect(seoChecks.detectedTags[H1]).to.have.lengthOf(1); +// }); +// }); +// +// describe('checkForTagsLength', () => { +// it('should detect tags that are too short or too long', () => { +// const pageTags = { +// [TITLE]: 'Short', +// [DESCRIPTION]: 'D'.repeat(200), // too long +// [H1]: ['Valid H1'], +// }; +// +// seoChecks.checkForTagsLength('https://example.com', pageTags); +// +// expect(seoChecks.detectedTags[TITLE]).to.have.lengthOf(1); +// expect(seoChecks.detectedTags[DESCRIPTION]).to.have.lengthOf(1); +// expect(seoChecks.detectedTags[H1]).to.have.lengthOf(0); +// }); +// }); +// +// describe('checkForH1Count', () => { +// it('should detect multiple H1 tags', () => { +// const pageTags = { +// [H1]: ['First H1', 'Second H1'], +// }; +// +// seoChecks.checkForH1Count('https://example.com', pageTags); +// +// expect(seoChecks.detectedTags[H1]).to.have.lengthOf(1); +// expect(seoChecks.detectedTags[H1][0]).to.deep.equal({ +// pageUrl: 'https://example.com', +// tagName: H1, +// tagContent: JSON.stringify(['First H1', 'Second H1']), +// seoImpact: MODERATE, +// seoOpportunityText: 'There are 2 H1 tags on this page, which is more than the recommended count of 1.', +// }); +// }); +// }); +// +// describe('checkForUniqueness', () => { +// it('should detect duplicate tags', () => { +// const pageTags1 = { +// [TITLE]: 'Duplicate Title', +// }; +// const pageTags2 = { +// [TITLE]: 'Duplicate Title', +// }; +// +// seoChecks.checkForUniqueness('https://page1.com', pageTags1); +// seoChecks.checkForUniqueness('https://page2.com', pageTags2); +// +// expect(seoChecks.detectedTags[TITLE]).to.have.lengthOf(1); +// expect(seoChecks.detectedTags[TITLE][0]).to.deep.equal({ +// pageUrl: 'https://page2.com', +// tagName: TITLE, +// tagContent: 'Duplicate Title', +// seoImpact: HIGH, +// seoOpportunityText: 'The title tag on this page is identical to the one on https://page1.com. It\'s recommended to have unique title tags for each page.', +// }); +// }); +// }); +// +// describe('Organize Detected Tags', () => { +// it('should sort non-unique H1 tags by count in descending order', () => { +// seoChecks.detectedTags = { +// h1: [ +// { +// [NON_UNIQUE]: { +// 'Tag A': { count: 3, urls: ['/url1', '/url2'] }, +// 'Tag B': { count: 5, urls: ['/url3'] }, +// 'Tag C': { count: 1, urls: ['/url4'] }, +// }, +// }, +// ], +// }; +// seoChecks.sortNonUniqueH1Tags(); +// const expected = { +// 'Tag B': { count: 5, urls: ['/url3'] }, +// 'Tag A': { count: 3, urls: ['/url1', '/url2'] }, +// 'Tag C': { count: 1, urls: ['/url4'] }, +// }; +// expect(seoChecks.detectedTags[H1][0][NON_UNIQUE]).to.deep.equal(expected); +// }); +// }); +// }); +// +// describe('handler method', () => { +// let message; +// let context; +// let logStub; +// let dataAccessStub; +// let s3ClientStub; +// +// beforeEach(() => { +// sinon.restore(); +// message = { type: 'seo', url: 'site-id' }; +// logStub = { info: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }; +// dataAccessStub = { +// getConfiguration: sinon.stub(), +// getTopPagesForSite: sinon.stub(), +// addAudit: sinon.stub(), +// retrieveSiteBySiteId: sinon.stub(), +// getSiteByID: sinon.stub().resolves({ isLive: sinon.stub().returns(true) }), +// }; +// s3ClientStub = { +// send: sinon.stub(), +// getObject: sinon.stub(), +// }; +// +// context = { +// log: logStub, +// dataAccess: dataAccessStub, +// s3Client: s3ClientStub, +// env: { S3_SCRAPER_BUCKET_NAME: 'test-bucket' }, +// }; +// }); +// +// it('should return notFound if site is not found', async () => { +// dataAccessStub.getSiteByID.resolves(null); +// +// const result = await auditMetaTags(message, context); +// expect(JSON.stringify(result)).to.equal(JSON.stringify(notFound('Site not found'))); +// expect(logStub.info.calledOnce).to.be.true; +// }); +// +// // it('should return ok if site is not live', async () => { +// // dataAccessStub.getSiteByID.resolves({ isLive: sinon.stub().returns(false) }); +// // +// // const result = await auditMetaTags(message, context); +// // expect(JSON.stringify(result)).to.equal(JSON.stringify(ok())); +// // expect(logStub.info.calledTwice).to.be.true; +// // }); +// +// it('should return ok if audit type is disabled for site', async () => { +// dataAccessStub.getConfiguration.resolves({ +// isHandlerEnabledForSite: sinon.stub().returns(false), +// }); +// const result = await auditMetaTags(message, context); +// expect(JSON.stringify(result)).to.equal(JSON.stringify(ok())); +// expect(logStub.info.calledTwice).to.be.true; +// }); +// +// it('should return notFound if extracted tags are not available', async () => { +// dataAccessStub.getConfiguration.resolves({ +// isHandlerEnabledForSite: sinon.stub().returns(true), +// }); +// s3ClientStub.send.returns([]); +// +// const result = await auditMetaTags(message, context); +// expect(JSON.stringify(result)).to.equal(JSON.stringify(notFound('Site tags data not available'))); +// expect(logStub.error.calledOnce).to.be.true; +// }); +// +// it('should process site tags and perform SEO checks', async () => { +// const site = { isLive: sinon.stub().returns(true), getId: sinon.stub().returns('site-id') }; +// const topPages = [{ getURL: 'http://example.com/blog/page1', getTopKeyword: sinon.stub().returns('page') }, +// { getURL: 'http://example.com/blog/page2', getTopKeyword: sinon.stub().returns('Test') }]; +// +// dataAccessStub.getSiteByID.resolves(site); +// dataAccessStub.getConfiguration.resolves({ +// isHandlerEnabledForSite: sinon.stub().returns(true), +// }); +// dataAccessStub.getTopPagesForSite.resolves(topPages); +// +// s3ClientStub.send +// .withArgs(sinon.match.instanceOf(ListObjectsV2Command).and(sinon.match.has('input', { +// Bucket: 'test-bucket', +// Prefix: 'scrapes/site-id/', +// MaxKeys: 1000, +// }))) +// .resolves({ +// Contents: [ +// { Key: 'scrapes/site-id/blog/page1/scrape.json' }, +// { Key: 'scrapes/site-id/blog/page2/scrape.json' }, +// ], +// }); +// +// s3ClientStub.send +// .withArgs(sinon.match.instanceOf(GetObjectCommand).and(sinon.match.has('input', { +// Bucket: 'test-bucket', +// Key: 'scrapes/site-id/blog/page1/scrape.json', +// }))).returns({ +// Body: { +// transformToString: () => JSON.stringify({ +// scrapeResult: { +// tags: { +// title: 'Test Page', +// description: '', +// }, +// }, +// }), +// }, +// }); +// s3ClientStub.send +// .withArgs(sinon.match.instanceOf(GetObjectCommand).and(sinon.match.has('input', { +// Bucket: 'test-bucket', +// Key: 'scrapes/site-id/blog/page2/scrape.json', +// }))).returns({ +// Body: { +// transformToString: () => JSON.stringify({ +// scrapeResult: { +// tags: { +// title: 'Test Page', +// h1: [ +// 'This is a dummy H1 that is overly length from SEO perspective', +// ], +// }, +// }, +// }), +// }, +// }); +// const addAuditStub = sinon.stub().resolves(); +// dataAccessStub.addAudit = addAuditStub; +// +// const result = await auditMetaTags(message, context); +// +// expect(JSON.stringify(result)).to.equal(JSON.stringify(noContent())); +// expect(addAuditStub.calledWithMatch({ +// title: [ +// { +// pageUrl: '/blog/page1', +// tagName: 'title', +// tagContent: 'Test Page', +// seoImpact: 'Moderate', +// seoOpportunityText: 'The title tag on this page has a length of 9 characters, which is below the recommended length of 40-60 characters.', +// }, +// { +// pageUrl: '/blog/page2', +// tagName: 'title', +// tagContent: 'Test Page', +// seoImpact: 'Moderate', +// seoOpportunityText: 'The title tag on this page has a length of 9 characters, which is below the recommended length of 40-60 characters.', +// }, +// { +// pageUrl: '/blog/page2', +// tagName: 'title', +// tagContent: 'Test Page', +// seoImpact: 'High', +// seoOpportunityText: "The title tag on this page is identical to the one on /blog/page1. It's recommended to have unique title tags for each page.", +// }, +// ], +// description: [ +// { +// pageUrl: '/blog/page1', +// tagName: 'description', +// tagContent: '', +// seoImpact: 'Moderate', +// seoOpportunityText: 'The description tag on this page has a length of 0 characters, which is below the recommended length of 140-160 characters.', +// }, +// { +// pageUrl: '/blog/page2', +// tagName: 'description', +// tagContent: '', +// seoImpact: 'High', +// seoOpportunityText: "The description tag on this page is missing. It's recommended to have a description tag on each page.", +// }, +// ], +// h1: [ +// { +// pageUrl: '/blog/page1', +// tagName: 'h1', +// tagContent: '', +// seoImpact: 'High', +// seoOpportunityText: "The h1 tag on this page is missing. It's recommended to have a h1 tag on each page.", +// }, +// { +// pageUrl: '/blog/page2', +// tagName: 'h1', +// tagContent: 'This is a dummy H1 that is overly length from SEO perspective', +// seoImpact: 'Moderate', +// seoOpportunityText: 'The h1 tag on this page has a length of 61 characters, which is above the recommended length of 60 characters.', +// }, +// ], +// })); +// expect(addAuditStub.calledOnce).to.be.true; +// expect(logStub.info.callCount).to.equal(4); +// }); +// +// it('should process site tags and perform SEO checks for pages with invalid H1s', async () => { +// const site = { isLive: sinon.stub().returns(true), getId: sinon.stub().returns('site-id') }; +// const topPages = [{ getURL: 'http://example.com/blog/page1', getTopKeyword: sinon.stub().returns('page') }, +// { getURL: 'http://example.com/blog/page2', getTopKeyword: sinon.stub().returns('Test') }]; +// +// dataAccessStub.getSiteByID.resolves(site); +// dataAccessStub.getConfiguration.resolves({ +// isHandlerEnabledForSite: sinon.stub().returns(true), +// }); +// dataAccessStub.getTopPagesForSite.resolves(topPages); +// +// s3ClientStub.send +// .withArgs(sinon.match.instanceOf(ListObjectsV2Command).and(sinon.match.has('input', { +// Bucket: 'test-bucket', +// Prefix: 'scrapes/site-id/', +// MaxKeys: 1000, +// }))) +// .resolves({ +// Contents: [ +// { Key: 'scrapes/site-id/blog/page1/scrape.json' }, +// { Key: 'scrapes/site-id/blog/page2/scrape.json' }, +// ], +// }); +// +// s3ClientStub.send +// .withArgs(sinon.match.instanceOf(GetObjectCommand).and(sinon.match.has('input', { +// Bucket: 'test-bucket', +// Key: 'scrapes/site-id/blog/page1/scrape.json', +// }))).returns({ +// Body: { +// transformToString: () => JSON.stringify({ +// scrapeResult: { +// tags: { +// title: 'This is an SEO optimal page1 valid title.', +// description: 'This is a dummy description that is optimal from SEO perspective for page1. It has the correct length of characters, and is unique across all pages.', +// h1: [ +// 'This is an overly long H1 tag from SEO perspective due to its length exceeding 60 chars', +// 'This is second h1 tag on same page', +// ], +// }, +// }, +// }), +// }, +// }); +// s3ClientStub.send +// .withArgs(sinon.match.instanceOf(GetObjectCommand).and(sinon.match.has('input', { +// Bucket: 'test-bucket', +// Key: 'scrapes/site-id/blog/page2/scrape.json', +// }))).returns({ +// Body: { +// transformToString: () => JSON.stringify({ +// scrapeResult: { +// tags: { +// title: 'This is a SEO wise optimised page2 title.', +// description: 'This is a dummy description that is optimal from SEO perspective for page2. It has the correct length of characters, and is unique across all pages.', +// h1: [ +// 'This is an overly long H1 tag from SEO perspective', +// ], +// }, +// }, +// }), +// }, +// }); +// const addAuditStub = sinon.stub().resolves(); +// dataAccessStub.addAudit = addAuditStub; +// +// const result = await auditMetaTags(message, context); +// +// expect(JSON.stringify(result)).to.equal(JSON.stringify(noContent())); +// expect(addAuditStub.calledWithMatch({ +// title: [], +// description: [], +// h1: [ +// { +// pageUrl: '/blog/page1', +// tagName: 'h1', +// tagContent: '', +// seoImpact: 'High', +// seoOpportunityText: "The h1 tag on this page is missing. It's recommended to have a h1 tag on each page.", +// }, +// { +// pageUrl: '/blog/page1', +// tagName: 'h1', +// seoImpact: 'High', +// seoOpportunityText: "The h1 tag on this page is missing the page's top keyword 'page'. It's recommended to include the primary keyword in the h1 tag.", +// }, +// { +// pageUrl: '/blog/page2', +// tagName: 'h1', +// tagContent: 'This is a dummy H1 that is overly length from SEO perspective', +// seoImpact: 'Moderate', +// seoOpportunityText: 'The h1 tag on this page has a length of 61 characters, which is above the recommended length of 60 characters.', +// }, +// { +// pageUrl: '/blog/page2', +// tagName: 'h1', +// tagContent: 'This is a dummy H1 that is overly length from SEO perspective', +// seoImpact: 'High', +// seoOpportunityText: "The h1 tag on this page is missing the page's top keyword 'test'. It's recommended to include the primary keyword in the h1 tag.", +// }, +// ], +// })); +// expect(addAuditStub.calledOnce).to.be.true; +// expect(logStub.info.callCount).to.equal(4); +// }); +// +// it('should handle errors and return internalServerError', async () => { +// dataAccessStub.getSiteByID.rejects(new Error('Some error')); +// +// const result = await auditMetaTags(message, context); +// expect(JSON.stringify(result)).to.equal(JSON.stringify(internalServerError('Internal server error: Some error'))); +// expect(logStub.error.calledOnce).to.be.true; +// }); +// +// it('should handle gracefully if S3 object has no rawbody', async () => { +// const site = { isLive: sinon.stub().returns(true), getId: sinon.stub().returns('site-id') }; +// const topPages = [{ getURL: 'http://example.com/blog/page1', getTopKeyword: sinon.stub().returns('page') }]; +// +// dataAccessStub.getSiteByID.resolves(site); +// dataAccessStub.getConfiguration.resolves({ +// isHandlerEnabledForSite: sinon.stub().returns(true), +// }); +// dataAccessStub.getTopPagesForSite.resolves(topPages); +// +// s3ClientStub.send +// .withArgs(sinon.match.instanceOf(ListObjectsV2Command).and(sinon.match.has('input', { +// Bucket: 'test-bucket', +// Prefix: 'scrapes/site-id/', +// MaxKeys: 1000, +// }))) +// .resolves({ +// Contents: [ +// { Key: 'scrapes/site-id/blog/page1.json' }, +// ], +// }); +// +// s3ClientStub.send +// .withArgs(sinon.match.instanceOf(GetObjectCommand).and(sinon.match.has('input', { +// Bucket: 'test-bucket', +// Key: 'scrapes/site-id/blog/page1.json', +// }))).returns({ +// Body: { +// transformToString: () => '', +// }, +// }); +// const addAuditStub = sinon.stub().resolves(); +// dataAccessStub.addAudit = addAuditStub; +// +// const result = await auditMetaTags(message, context); +// +// expect(JSON.stringify(result)).to.equal(JSON.stringify(notFound('Site tags data not available'))); +// expect(addAuditStub.calledOnce).to.be.false; +// expect(logStub.error.calledThrice).to.be.true; +// }); +// +// it('should handle gracefully if S3 tags object is not valid', async () => { +// const site = { isLive: sinon.stub().returns(true), getId: sinon.stub().returns('site-id') }; +// const topPages = [{ getURL: 'http://example.com/blog/page1', getTopKeyword: sinon.stub().returns('page') }, +// { getURL: 'http://example.com/blog/page2', getTopKeyword: sinon.stub().returns('Test') }]; +// +// dataAccessStub.getSiteByID.resolves(site); +// dataAccessStub.getConfiguration.resolves({ +// isHandlerEnabledForSite: sinon.stub().returns(true), +// }); +// dataAccessStub.getTopPagesForSite.resolves(topPages); +// +// s3ClientStub.send +// .withArgs(sinon.match.instanceOf(ListObjectsV2Command).and(sinon.match.has('input', { +// Bucket: 'test-bucket', +// Prefix: 'scrapes/site-id/', +// MaxKeys: 1000, +// }))) +// .resolves({ +// Contents: [ +// { Key: 'page1.json' }, +// ], +// }); +// +// s3ClientStub.send +// .withArgs(sinon.match.instanceOf(GetObjectCommand)) +// .returns({ +// Body: { +// transformToString: () => JSON.stringify({ +// scrapeResult: { +// tags: 5, +// }, +// }), +// }, +// }); +// const result = await auditMetaTags(message, context); +// +// expect(JSON.stringify(result)).to.equal(JSON.stringify(notFound('Site tags data not available'))); +// expect(logStub.error.calledTwice).to.be.true; +// }); +// }); +// });