Skip to content

Commit

Permalink
Add SEO tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Matej Jellus authored and Matej Jellus committed Jun 21, 2023
1 parent 7dfb0cd commit 9253baf
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 1 deletion.
5 changes: 5 additions & 0 deletions src/Pentest.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import Security from './security';
import HTML from './html';
import SEO from './seo';
import WordPress from './wordpress';
import { Result } from './Test';

interface PentestResult {
security: Result;
html: Result;
seo: Result;
wordpress: Result;
}

class Pentest {
public async run(url: string): Promise<PentestResult> {
const security = new Security();
const html = new HTML();
const seo = new SEO();
const wordPress = new WordPress();
const securityResult = <Result> await security.run({ url });
const htmlResult = <Result> await html.run({ url });
const seoResult = <Result> await seo.run({ url });
const wordPressResult = <Result> await wordPress.run({ url });

return {
security: securityResult,
html: htmlResult,
seo: seoResult,
wordpress: wordPressResult,
};
}
Expand Down
13 changes: 13 additions & 0 deletions src/functions/getHeading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { URL } from 'url';
import getObject from './getObject';

const getHeading = (result: any): string | string[] => {
const titles = getObject(result.html, 'name', 'h1')
.map((title: any) => {
return title.children[0].data;
});

return titles.length > 1 ? titles : titles.shift();
};

export default getHeading;
13 changes: 13 additions & 0 deletions src/functions/getTitle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { URL } from 'url';
import getObject from './getObject';

const getTitle = (result: any): string | string[] => {
const titles = getObject(result.html, 'name', 'title')
.map((title: any) => {
return title.children[0].data;
});

return titles.length > 1 ? titles : titles.shift();
};

export default getTitle;
2 changes: 2 additions & 0 deletions src/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export { default as getGenerator } from './getGenerator';
export { default as parseHtml } from './parseHtml';
export { default as parseSitemap } from './parseSitemap';
export { default as parseXml } from './parseXml';
export { default as getTitle } from './getTitle';
export { default as getHeading } from './getHeading';
4 changes: 4 additions & 0 deletions src/functions/parseXml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import parser from 'xml2js';
export default function(result) {
return new Promise((resolve, reject) => {
parser.parseString(result.body, (err, r) => {
if (err) {
reject(err);
}

resolve(r);
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ program
const results = await pentest.run(url);

const report = Report.get(config.report.format);
report.write([ results.security, results.html, results.wordpress ]);
report.write([ results.security, results.html, results.seo, results.wordpress ]);
});

program
Expand Down
49 changes: 49 additions & 0 deletions src/seo/Heading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Test, { TestParameters, Result } from '../Test';
import request from '../request';
import logger from '../logger';
import { getHeading, parseHtml } from '../functions';

class Heading extends Test {
public name = 'Heading';

public async test({ url }: TestParameters): Promise<Result> {
logger.info(`Starting ${this.constructor.name} test...`);

const response = await request.get(url);
const html = await parseHtml(response);
const heading = getHeading(html);
const subTests = this.checkHeading(heading);

return {
status: this.getStatus(subTests.map(test => test.status)),
title: this.constructor.name,
description: '',
results: subTests,
};
}

private checkHeading(title: string | string[]): Array<Result> {
const results = [];

results.push({
status: typeof title !== undefined && title.length > 0 ? 'SUCCESS' : 'WARNING',
title: 'H1 tag',
});

results.push({
status: Array.isArray(title) ? 'ERROR' : 'SUCCESS',
title: 'Duplicate H1 tag',
description: `HTML should contain just one title tag.`,
});

results.push({
status: title.length <= 60 ? 'SUCCESS' : 'WARNING',
title: 'Title length',
description: `Title length should be under 60 characters and it is ${title.length}.`,
});

return results;
}
}

export default Heading;
21 changes: 21 additions & 0 deletions src/seo/Robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Test, { TestParameters, Result } from '../Test';
import request from '../request';
import logger from '../logger';

class Robots extends Test {
public name = 'Robots';

public async test({ url }: TestParameters): Promise<Result> {
logger.info(`Starting ${this.constructor.name} test...`);

const response = await request.get(`${url}/robots.txt`);

return {
status: Math.floor(response.statusCode / 100) === 2 ? 'SUCCESS' : 'WARNING',
title: 'Robots.txt',
description: '',
};
}
}

export default Robots;
37 changes: 37 additions & 0 deletions src/seo/Sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Test, { TestParameters, Result } from '../Test';
import request from '../request';
import logger from '../logger';
import { parseXml } from '../functions';

class Sitemap extends Test {
public name = 'Sitemap';

public async test({ url }: TestParameters): Promise<Result> {
logger.info(`Starting ${this.constructor.name} test...`);

const robotsResponse = await request.get(`${url}/robots.txt`);

let sitemapUrl = `${url}/sitemap.xml`;

if (Math.floor(robotsResponse.statusCode / 100) === 2) {
const lines = robotsResponse.body.split(/\r?\n/);

const sitemap = lines.find(line => line.startsWith('Sitemap'));

if (typeof sitemap !== 'undefined') {
sitemapUrl = sitemap.split(' ')[1];
}
}

const response = await request.get(sitemapUrl);
const xml = await parseXml(response) as object;

return {
status: 'sitemapindex' in xml || 'urlset' in xml ? 'SUCCESS' : 'WARNING',
title: this.constructor.name,
description: '',
};
}
}

export default Sitemap;
49 changes: 49 additions & 0 deletions src/seo/Title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Test, { TestParameters, Result } from '../Test';
import request from '../request';
import logger from '../logger';
import { getTitle, parseHtml } from '../functions';

class Title extends Test {
public name = 'Title';

public async test({ url }: TestParameters): Promise<Result> {
logger.info(`Starting ${this.constructor.name} test...`);

const response = await request.get(url);
const html = await parseHtml(response);
const title = getTitle(html);
const subTests = this.checkTitle(title);

return {
status: this.getStatus(subTests.map(test => test.status)),
title: this.constructor.name,
description: '',
results: subTests,
};
}

private checkTitle(title: string | string[]): Array<Result> {
const results = [];

results.push({
status: typeof title !== undefined && title.length > 0 ? 'SUCCESS' : 'WARNING',
title: 'Title tag',
});

results.push({
status: Array.isArray(title) ? 'ERROR' : 'SUCCESS',
title: 'Duplicate title tag',
description: `HTML should contain just one title tag.`,
});

results.push({
status: title.length <= 60 ? 'SUCCESS' : 'WARNING',
title: 'Title length',
description: `Title length should be under 60 characters and it is ${title.length}.`,
});

return results;
}
}

export default Title;
47 changes: 47 additions & 0 deletions src/seo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Test, { TestParameters, Result } from '../Test';
import Title from './Title';
import Heading from './Heading';
import Sitemap from './Sitemap';
import Robots from './Robots';

export default class SEO extends Test {
public name = 'SEO';

constructor() {
super();
this.tests = [
new Title(),
new Heading(),
new Sitemap(),
new Robots(),
];
}

public async test(params: TestParameters): Promise<Result> {
const tests = this.getTests();
const results = [];

for (const test of tests) {
let result = null;

try {
result = await test.run(params);
} catch (error) {
result = {
status: 'ERROR',
title: test.name,
description: 'Test failed or cannot be run!',
}
}

results.push(result);
}

return {
status: this.getStatus(results.map(result => result.status)),
title: this.name,
description: '',
results,
};
}
}

0 comments on commit 9253baf

Please sign in to comment.