From 3140b1ff11772281696685644444766eb0eefb67 Mon Sep 17 00:00:00 2001 From: MattIPv4 Date: Tue, 14 Jan 2020 12:52:55 +0000 Subject: [PATCH 1/7] Move test check helpers to helpers/checks.js --- test/Routes/API.js | 16 +++++++------- test/Routes/Index.js | 8 +++---- test/Routes/Lists.js | 48 +++++++++++++++++++++--------------------- test/Routes/Tasks.js | 6 +++--- test/Routes/Test.js | 10 ++++----- test/base.js | 46 ++-------------------------------------- test/helpers/checks.js | 42 ++++++++++++++++++++++++++++++++++++ 7 files changed, 88 insertions(+), 88 deletions(-) create mode 100644 test/helpers/checks.js diff --git a/test/Routes/API.js b/test/Routes/API.js index 831b1e4..658d45d 100644 --- a/test/Routes/API.js +++ b/test/Routes/API.js @@ -1,4 +1,4 @@ -const { describe, it, expect, request, ratelimitBypass, resetRatelimits, ratelimitTest, locale, titleCheck, db } = require('../base'); +const { describe, it, expect, request, ratelimitBypass, resetRatelimits, checks, locale, db } = require('../base'); const listProps = require('../../src/Util/listProps'); describe('Invalid route (/api/helloworld)', () => { @@ -71,7 +71,7 @@ describe('/api/docs', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `API Docs - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `API Docs - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -191,7 +191,7 @@ describe('/api/docs/libs', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `Libraries - API Docs - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `Libraries - API Docs - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -355,7 +355,7 @@ describe('/api/lists', () => { }); }); it('does not ratelimit requests spaced correctly', function (done) { - ratelimitTest(this, 1, test, done); + checks.ratelimit(this, 1, test, done); }); }); @@ -505,7 +505,7 @@ describe('/api/lists/:id', () => { }); }); it('does not ratelimit requests spaced correctly', function (done) { - ratelimitTest(this, 1, test, done, 404); + checks.ratelimit(this, 1, test, done, 404); }); }); @@ -1069,7 +1069,7 @@ describe('/api/count', () => { bot_id: '123456789123456789', server_count: 10 }); - ratelimitTest(this, 120, test, done); + checks.ratelimit(this, 120, test, done); }); }); }); @@ -1281,7 +1281,7 @@ describe('/api/bots/:id', () => { }); }); it('does not ratelimit requests spaced correctly', function (done) { - ratelimitTest(this, 30, test, done, 400); + checks.ratelimit(this, 30, test, done, 400); }); }); @@ -1397,7 +1397,7 @@ describe('/api/legacy-ids', () => { }); }); it('does not ratelimit requests spaced correctly', function (done) { - ratelimitTest(this, 1, test, done); + checks.ratelimit(this, 1, test, done); }); }); diff --git a/test/Routes/Index.js b/test/Routes/Index.js index c408a98..f5d636b 100644 --- a/test/Routes/Index.js +++ b/test/Routes/Index.js @@ -1,4 +1,4 @@ -const { describe, it, expect, request, db, locale, titleCheck } = require('../base'); +const { describe, it, expect, request, db, locale, checks } = require('../base'); const renderer = new (require('../../src/Structure/Markdown'))(); describe('/', () => { @@ -13,7 +13,7 @@ describe('/', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -46,7 +46,7 @@ describe('/helloworld', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `Page not found - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `Page not found - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -73,7 +73,7 @@ describe('/about', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `About - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `About - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); diff --git a/test/Routes/Lists.js b/test/Routes/Lists.js index ff14032..f5bfa19 100644 --- a/test/Routes/Lists.js +++ b/test/Routes/Lists.js @@ -1,4 +1,4 @@ -const { describe, it, expect, request, db, locale, titleCheck, authCheck } = require('../base'); +const { describe, it, expect, request, db, locale, checks, authAsMod } = require('../base'); describe('/lists', () => { describe('GET', () => { @@ -12,7 +12,7 @@ describe('/lists', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `All Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `All Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -79,7 +79,7 @@ describe('/lists/new', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `New Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `New Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -123,7 +123,7 @@ describe('/lists/defunct', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `Defunct Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `Defunct Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -163,7 +163,7 @@ describe('/lists/hidden', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `Hidden Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `Hidden Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -203,7 +203,7 @@ describe('/lists/features', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `All List Features - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `All List Features - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -240,7 +240,7 @@ describe('/lists/features/:id', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `Has Voting - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `Has Voting - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -290,7 +290,7 @@ describe('/lists/search', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `Bot List Search - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `Bot List Search - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -328,7 +328,7 @@ describe('/lists/search/:query', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `Bot List Search - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `Bot List Search - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -387,7 +387,7 @@ describe('/lists/:id', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `${data.name} (${data.id}) - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `${data.name} (${data.id}) - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -472,7 +472,7 @@ describe('/lists/:id', () => { test().end((err, res) => { expect(res).to.have.status(200); expect(res).to.be.html; - titleCheck(res, `${data.name} (${data.id}) - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `${data.name} (${data.id}) - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); @@ -509,7 +509,7 @@ describe('/lists/:id/edit', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -525,7 +525,7 @@ describe('/lists/:id/edit', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -544,7 +544,7 @@ describe('/lists/:id/icon', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -562,7 +562,7 @@ describe('/lists/add', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -578,7 +578,7 @@ describe('/lists/add', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -596,7 +596,7 @@ describe('/lists/legacy-ids', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -612,7 +612,7 @@ describe('/lists/legacy-ids', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -630,7 +630,7 @@ describe('/lists/features/manage', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -648,7 +648,7 @@ describe('/lists/features/manage/add', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -664,7 +664,7 @@ describe('/lists/features/manage/add', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -682,7 +682,7 @@ describe('/lists/features/manage/:id', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -698,7 +698,7 @@ describe('/lists/features/manage/:id', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -716,7 +716,7 @@ describe('/lists/features/manage/:id/delete', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); diff --git a/test/Routes/Tasks.js b/test/Routes/Tasks.js index 15457f6..569a705 100644 --- a/test/Routes/Tasks.js +++ b/test/Routes/Tasks.js @@ -1,4 +1,4 @@ -const { describe, it, expect, request, authCheck } = require('../base'); +const { describe, it, expect, request, checks } = require('../base'); describe('/tasks', () => { describe('GET', () => { @@ -11,7 +11,7 @@ describe('/tasks', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -29,7 +29,7 @@ describe('/tasks/run/:id', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); diff --git a/test/Routes/Test.js b/test/Routes/Test.js index b9fe65b..2635eec 100644 --- a/test/Routes/Test.js +++ b/test/Routes/Test.js @@ -1,4 +1,4 @@ -const { describe, it, expect, request, authCheck } = require('../base'); +const { describe, it, expect, request, checks } = require('../base'); describe('/test', () => { describe('GET', () => { @@ -11,7 +11,7 @@ describe('/test', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -29,7 +29,7 @@ describe('/test/start', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -47,7 +47,7 @@ describe('/test/restart', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); @@ -65,7 +65,7 @@ describe('/test/progress', () => { }); it('renders the authentication required message', done => { test().end((err, res) => { - authCheck(res); + checks.authRequired(res); done(); }); }); diff --git a/test/base.js b/test/base.js index 4f4517f..abb592d 100644 --- a/test/base.js +++ b/test/base.js @@ -1,6 +1,7 @@ const config = require('../config'); const i18n = require('../src/Util/i18n'); const db = require('../db/db')(); +const checks = require('./helpers/checks'); const { describe, it } = require('mocha'); @@ -14,49 +15,8 @@ const request = () => chai.request(target); const ratelimitBypass = (req) => req.set('X-Ratelimit-Bypass', config.secret); const resetRatelimits = () => ratelimitBypass(request().get('/api/reset')); -const ratelimitTest = (context, limit, test, done, status = 200) => { - context.retries(0); - context.slow((limit * 1.15 + 1.5) * 1000); - context.timeout((limit * 1.25 + 3) * 1000); - resetRatelimits().end(() => { - test().end(() => { - }); - setTimeout(() => { - test().end((err, res) => { - expect(res).to.have.status(status); - expect(res).to.be.json; - done(); - }); - }, (limit * 1.05 + 0.5) * 1000); - }); -}; - const locale = i18n.__; -const authCheck = res => { - expect(res).to.be.html; - expect(res.text).to.include('This page requires authentication to access'); - expect(res.text).to.include(`Sign in to ${locale('site_name')}`); - expect(res.text).to.include('A 403 error has occurred... :('); -}; - -const titleCheck = (res, expectedTitle) => { - expect(res).to.be.html; - - // Generic - expect(res.text).to.include(`${expectedTitle}`); - expect(res.text).to.include(``); - - // OG - expect(res.text).to.include(``); - expect(res.text).to.include(``); - expect(res.text).to.include(``); - - // Twitter - expect(res.text).to.include(``); - expect(res.text).to.include(``); -}; - const compareObjectProps = (a, b) => { const missing = []; const aProps = Object.keys(a); @@ -88,10 +48,8 @@ module.exports = { request, ratelimitBypass, resetRatelimits, - ratelimitTest, db, locale, - authCheck, - titleCheck, + checks, compareObjects }; diff --git a/test/helpers/checks.js b/test/helpers/checks.js new file mode 100644 index 0000000..f29c577 --- /dev/null +++ b/test/helpers/checks.js @@ -0,0 +1,42 @@ +const ratelimit = (context, limit, test, done, status = 200) => { + context.retries(0); + context.slow((limit * 1.15 + 1.5) * 1000); + context.timeout((limit * 1.25 + 3) * 1000); + resetRatelimits().end(() => { + test().end(() => { + }); + setTimeout(() => { + test().end((err, res) => { + expect(res).to.have.status(status); + expect(res).to.be.json; + done(); + }); + }, (limit * 1.05 + 0.5) * 1000); + }); +}; + +const authRequired = res => { + expect(res).to.be.html; + expect(res.text).to.include('This page requires authentication to access'); + expect(res.text).to.include(`Sign in to ${locale('site_name')}`); + expect(res.text).to.include('A 403 error has occurred... :('); +}; + +const title = (res, expectedTitle) => { + expect(res).to.be.html; + + // Generic + expect(res.text).to.include(`${expectedTitle}`); + expect(res.text).to.include(``); + + // OG + expect(res.text).to.include(``); + expect(res.text).to.include(``); + expect(res.text).to.include(``); + + // Twitter + expect(res.text).to.include(``); + expect(res.text).to.include(``); +}; + +module.exports = { ratelimit, authRequired, title }; From 655281465c85b8ac177e738fddc3564f3600323e Mon Sep 17 00:00:00 2001 From: MattIPv4 Date: Tue, 14 Jan 2020 12:58:35 +0000 Subject: [PATCH 2/7] Condense auth tests --- test/Routes/Lists.js | 104 ++++++----------------------------------- test/Routes/Tasks.js | 16 +------ test/Routes/Test.js | 32 ++----------- test/helpers/checks.js | 1 + 4 files changed, 20 insertions(+), 133 deletions(-) diff --git a/test/Routes/Lists.js b/test/Routes/Lists.js index f5bfa19..e76974e 100644 --- a/test/Routes/Lists.js +++ b/test/Routes/Lists.js @@ -501,13 +501,7 @@ describe('/lists/:id/edit', () => { const listId = 'botlist.space'; describe(`GET (:id = ${listId})`, () => { const test = () => request().get(`/lists/${listId}/edit`); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -517,13 +511,7 @@ describe('/lists/:id/edit', () => { describe(`POST (:id = ${listId})`, () => { const test = () => request().post(`/lists/${listId}/edit`); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -536,13 +524,7 @@ describe('/lists/:id/icon', () => { const listId = 'botlist.space'; describe(`GET (:id = ${listId})`, () => { const test = () => request().get(`/lists/${listId}/icon`); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -554,13 +536,7 @@ describe('/lists/:id/icon', () => { describe('/lists/add', () => { describe('GET', () => { const test = () => request().get('/lists/add'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -570,13 +546,7 @@ describe('/lists/add', () => { describe('POST', () => { const test = () => request().post('/lists/add'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -588,13 +558,7 @@ describe('/lists/add', () => { describe('/lists/legacy-ids', () => { describe('GET', () => { const test = () => request().get('/lists/legacy-ids'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -604,13 +568,7 @@ describe('/lists/legacy-ids', () => { describe('POST', () => { const test = () => request().post('/lists/legacy-ids'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -622,13 +580,7 @@ describe('/lists/legacy-ids', () => { describe('/lists/features/manage', () => { describe('GET', () => { const test = () => request().get('/lists/features/manage'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -640,13 +592,7 @@ describe('/lists/features/manage', () => { describe('/lists/features/manage/add', () => { describe('GET', () => { const test = () => request().get('/lists/features/manage/add'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -656,13 +602,7 @@ describe('/lists/features/manage/add', () => { describe('POST', () => { const test = () => request().post('/lists/features/manage/add'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -674,13 +614,7 @@ describe('/lists/features/manage/add', () => { describe('/lists/features/manage/:id', () => { describe('GET', () => { const test = () => request().get('/lists/features/manage/1'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -690,13 +624,7 @@ describe('/lists/features/manage/:id', () => { describe('POST', () => { const test = () => request().post('/lists/features/manage/1'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -708,13 +636,7 @@ describe('/lists/features/manage/:id', () => { describe('/lists/features/manage/:id/delete', () => { describe('GET', () => { const test = () => request().get('/lists/features/manage/1/delete'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); diff --git a/test/Routes/Tasks.js b/test/Routes/Tasks.js index 569a705..64d0866 100644 --- a/test/Routes/Tasks.js +++ b/test/Routes/Tasks.js @@ -3,13 +3,7 @@ const { describe, it, expect, request, checks } = require('../base'); describe('/tasks', () => { describe('GET', () => { const test = () => request().get('/tasks'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -21,13 +15,7 @@ describe('/tasks', () => { describe('/tasks/run/:id', () => { describe('POST', () => { const test = () => request().post('/tasks/run/0'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); diff --git a/test/Routes/Test.js b/test/Routes/Test.js index 2635eec..fdedaad 100644 --- a/test/Routes/Test.js +++ b/test/Routes/Test.js @@ -3,13 +3,7 @@ const { describe, it, expect, request, checks } = require('../base'); describe('/test', () => { describe('GET', () => { const test = () => request().get('/test'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -21,13 +15,7 @@ describe('/test', () => { describe('/test/start', () => { describe('GET', () => { const test = () => request().get('/test/start'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -39,13 +27,7 @@ describe('/test/start', () => { describe('/test/restart', () => { describe('GET', () => { const test = () => request().get('/test/restart'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); @@ -57,13 +39,7 @@ describe('/test/restart', () => { describe('/test/progress', () => { describe('GET', () => { const test = () => request().get('/test/progress'); - it('returns a Forbidden status code', done => { - test().end((err, res) => { - expect(res).to.have.status(403); - done(); - }); - }); - it('renders the authentication required message', done => { + it('returns the authentication required message', done => { test().end((err, res) => { checks.authRequired(res); done(); diff --git a/test/helpers/checks.js b/test/helpers/checks.js index f29c577..320ea11 100644 --- a/test/helpers/checks.js +++ b/test/helpers/checks.js @@ -16,6 +16,7 @@ const ratelimit = (context, limit, test, done, status = 200) => { }; const authRequired = res => { + expect(res).to.have.status(403); expect(res).to.be.html; expect(res.text).to.include('This page requires authentication to access'); expect(res.text).to.include(`Sign in to ${locale('site_name')}`); From e319031f75a4e2a2eb0a94b5ccd041d6f424bc64 Mon Sep 17 00:00:00 2001 From: MattIPv4 Date: Tue, 14 Jan 2020 13:28:26 +0000 Subject: [PATCH 3/7] Allow the test suite to authenticate as different users --- src/Structure/BaseRoute.js | 6 ++-- src/Website.js | 28 ++++++++++++++++- test/Routes/Lists.js | 63 ++++++++++++++++++++++++++++++-------- test/base.js | 16 +++------- test/helpers/auth.js | 8 +++++ test/helpers/checks.js | 3 ++ test/helpers/request.js | 16 ++++++++++ 7 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 test/helpers/auth.js create mode 100644 test/helpers/request.js diff --git a/src/Structure/BaseRoute.js b/src/Structure/BaseRoute.js index 4ec7c19..9710f82 100644 --- a/src/Structure/BaseRoute.js +++ b/src/Structure/BaseRoute.js @@ -10,12 +10,14 @@ class BaseRoute { isMod(req, res, next) { if (req.session.user.mod || req.session.user.admin) return next(); - res.render('error', { title: 'Page not found', status: 404, message: 'The page you were looking for could not be found.' }); + // res.render('error', { title: 'Page not found', status: 404, message: 'The page you were looking for could not be found.' }); + res.status(403).render('authRequired', { title: 'Authentication is required' }) } isAdmin(req, res, next) { if (req.session.user.admin) return next(); - res.render('error', { title: 'Page not found', status: 404, message: 'The page you were looking for could not be found.' }); + // res.render('error', { title: 'Page not found', status: 404, message: 'The page you were looking for could not be found.' }); + res.status(403).render('authRequired', { title: 'Authentication is required' }) } } diff --git a/src/Website.js b/src/Website.js index 77a0fe7..adfe050 100644 --- a/src/Website.js +++ b/src/Website.js @@ -38,13 +38,39 @@ class Website { this.app.use('/assets', express.static(path.join(__dirname, 'Assets'))); this.app.use('/codemirror', express.static(path.join(__dirname, '..', 'node_modules', 'codemirror'))); this.app.use((req, res, next) => { + // Test suite logic + res.locals.adblock = req.headers['x-disable-adsense'] === config.secret; + if (req.headers['x-auth-as-user'] === config.secret || + req.headers['x-auth-as-admin'] === config.secret || + req.headers['x-auth-as-mod'] === config.secret) { + req.session.user = { + id: '123456789012345678', + username: 'User', + avatar: '', + discriminator: '1234', + locale: 'en-US', + mfa_enabled: false, + flags: 0, + access_token: '', + expires_in: 604800, + refresh_token: '', + scope: 'identify', + token_type: 'Bearer', + admin: false, + mod: false + }; + if (req.headers['x-auth-as-mod'] || req.headers['x-auth-as-admin']) req.session.user.mod = true; + if (req.headers['x-auth-as-admin']) req.session.user.admin = true; + } + if (req.headers['x-auth-as-anon'] === config.secret) req.session.user = undefined; + + // App locals const host = req.get('host'); res.locals.route = req.connection.encrypted ? 'https://' : 'http://' + host + req.path; res.locals.isProduction = host.toLowerCase().trim() === 'botblock.org'; res.locals.isStaging = host.toLowerCase().trim() === 'staging.botblock.org'; res.locals.isDevelopment = !res.locals.isProduction && !res.locals.isStaging; res.locals.language = req.cookies.lang; - res.locals.adblock = req.headers['x-disable-adsense'] && req.headers['x-disable-adsense'] === config.secret; res.locals.breadcrumb = req.path.split('/').splice(1, 3, null); res.locals.user = req.session.user; res.cookie('url', req.path.startsWith('/auth') ? '/' : req.path); diff --git a/test/Routes/Lists.js b/test/Routes/Lists.js index e76974e..7867199 100644 --- a/test/Routes/Lists.js +++ b/test/Routes/Lists.js @@ -1,4 +1,4 @@ -const { describe, it, expect, request, db, locale, checks, authAsMod } = require('../base'); +const { describe, it, expect, request, db, locale, checks, auth } = require('../base'); describe('/lists', () => { describe('GET', () => { @@ -499,22 +499,59 @@ describe('/lists/:id', () => { describe('/lists/:id/edit', () => { const listId = 'botlist.space'; - describe(`GET (:id = ${listId})`, () => { - const test = () => request().get(`/lists/${listId}/edit`); - it('returns the authentication required message', done => { - test().end((err, res) => { - checks.authRequired(res); - done(); + describe('As an anonymous user', () => { + describe(`GET (:id = ${listId})`, () => { + const test = () => request().get(`/lists/${listId}/edit`); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + + describe(`POST (:id = ${listId})`, () => { + const test = () => request().post(`/lists/${listId}/edit`); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); }); }); }); - describe(`POST (:id = ${listId})`, () => { - const test = () => request().post(`/lists/${listId}/edit`); - it('returns the authentication required message', done => { - test().end((err, res) => { - checks.authRequired(res); - done(); + describe('As a logged in user', () => { + describe(`GET (:id = ${listId})`, () => { + const test = () => auth.asUser(request().get(`/lists/${listId}/edit`)); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + + describe(`POST (:id = ${listId})`, () => { + const test = () => auth.asUser(request().post(`/lists/${listId}/edit`)); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + }); + + describe('As a moderator', () => { + describe(`GET (:id = ${listId})`, () => { + const test = () => auth.asMod(request().get(`/lists/${listId}/edit`)); + it('renders the edit page', done => { + test().end((err, res) => { + expect(res).to.have.status(200); + expect(res).to.be.html; + done(); + }); }); }); }); diff --git a/test/base.js b/test/base.js index abb592d..172c0a3 100644 --- a/test/base.js +++ b/test/base.js @@ -1,22 +1,16 @@ const config = require('../config'); -const i18n = require('../src/Util/i18n'); +const locale = require('../src/Util/i18n').__; const db = require('../db/db')(); const checks = require('./helpers/checks'); +const auth = require('./helpers/auth'); +const request = require('./helpers/request'); const { describe, it } = require('mocha'); +const { expect } = require('chai'); -const chai = require('chai'); -const chaiHttp = require('chai-http'); -const expect = chai.expect; -chai.use(chaiHttp); - -const target = `${config.baseURL}:${config.port}`; -const request = () => chai.request(target); const ratelimitBypass = (req) => req.set('X-Ratelimit-Bypass', config.secret); const resetRatelimits = () => ratelimitBypass(request().get('/api/reset')); -const locale = i18n.__; - const compareObjectProps = (a, b) => { const missing = []; const aProps = Object.keys(a); @@ -43,7 +37,6 @@ module.exports = { describe, it, expect, - target, secret: config.secret, request, ratelimitBypass, @@ -51,5 +44,6 @@ module.exports = { db, locale, checks, + auth, compareObjects }; diff --git a/test/helpers/auth.js b/test/helpers/auth.js new file mode 100644 index 0000000..05d5a30 --- /dev/null +++ b/test/helpers/auth.js @@ -0,0 +1,8 @@ +const config = require('../../config'); + +const asAnon = (req) => req.set('X-Auth-As-Anon', config.secret); +const asUser = (req) => req.set('X-Auth-As-User', config.secret).unset('X-Auth-As-Anon'); +const asMod = (req) => req.set('X-Auth-As-Mod', config.secret).unset('X-Auth-As-Anon'); +const asAdmin = (req) => req.set('X-Auth-As-Admin', config.secret).unset('X-Auth-As-Anon'); + +module.exports = { asAnon, asUser, asMod, asAdmin }; diff --git a/test/helpers/checks.js b/test/helpers/checks.js index 320ea11..257a5e1 100644 --- a/test/helpers/checks.js +++ b/test/helpers/checks.js @@ -1,3 +1,6 @@ +const { expect } = require('chai'); +const locale = require('../../src/Util/i18n').__; + const ratelimit = (context, limit, test, done, status = 200) => { context.retries(0); context.slow((limit * 1.15 + 1.5) * 1000); diff --git a/test/helpers/request.js b/test/helpers/request.js new file mode 100644 index 0000000..ab9ee46 --- /dev/null +++ b/test/helpers/request.js @@ -0,0 +1,16 @@ +const config = require('../../config'); +const { asAnon } = require('./auth'); +const chai = require('chai'); +const chaiHttp = require('chai-http'); +chai.use(chaiHttp); + +const target = `${config.baseURL}:${config.port}`; +const base = () => chai.request(target); + +module.exports = () => new Proxy(base, { + get(target, method) { + return function (...args) { + return asAnon(target()[method].apply(this, args)); + }; + } +}); From 17ec78e65bcad13f1d5a6c3f639a544b885c81cb Mon Sep 17 00:00:00 2001 From: MattIPv4 Date: Tue, 14 Jan 2020 13:45:32 +0000 Subject: [PATCH 4/7] Fix errors --- src/Structure/BaseRoute.js | 4 ++-- test/Routes/Tasks.js | 2 +- test/Routes/Test.js | 2 +- test/base.js | 8 +++----- test/helpers/checks.js | 1 + test/helpers/ratelimits.js | 7 +++++++ 6 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 test/helpers/ratelimits.js diff --git a/src/Structure/BaseRoute.js b/src/Structure/BaseRoute.js index 9710f82..5cb62f6 100644 --- a/src/Structure/BaseRoute.js +++ b/src/Structure/BaseRoute.js @@ -11,13 +11,13 @@ class BaseRoute { isMod(req, res, next) { if (req.session.user.mod || req.session.user.admin) return next(); // res.render('error', { title: 'Page not found', status: 404, message: 'The page you were looking for could not be found.' }); - res.status(403).render('authRequired', { title: 'Authentication is required' }) + res.status(403).render('authRequired', { title: 'Authentication is required' }); } isAdmin(req, res, next) { if (req.session.user.admin) return next(); // res.render('error', { title: 'Page not found', status: 404, message: 'The page you were looking for could not be found.' }); - res.status(403).render('authRequired', { title: 'Authentication is required' }) + res.status(403).render('authRequired', { title: 'Authentication is required' }); } } diff --git a/test/Routes/Tasks.js b/test/Routes/Tasks.js index 64d0866..d724c4c 100644 --- a/test/Routes/Tasks.js +++ b/test/Routes/Tasks.js @@ -1,4 +1,4 @@ -const { describe, it, expect, request, checks } = require('../base'); +const { describe, it, request, checks } = require('../base'); describe('/tasks', () => { describe('GET', () => { diff --git a/test/Routes/Test.js b/test/Routes/Test.js index fdedaad..aee4b94 100644 --- a/test/Routes/Test.js +++ b/test/Routes/Test.js @@ -1,4 +1,4 @@ -const { describe, it, expect, request, checks } = require('../base'); +const { describe, it, request, checks } = require('../base'); describe('/test', () => { describe('GET', () => { diff --git a/test/base.js b/test/base.js index 172c0a3..9ac3647 100644 --- a/test/base.js +++ b/test/base.js @@ -4,12 +4,10 @@ const db = require('../db/db')(); const checks = require('./helpers/checks'); const auth = require('./helpers/auth'); const request = require('./helpers/request'); - +const { ratelimitBypass, resetRatelimits } = require('./helpers/ratelimits'); const { describe, it } = require('mocha'); -const { expect } = require('chai'); - -const ratelimitBypass = (req) => req.set('X-Ratelimit-Bypass', config.secret); -const resetRatelimits = () => ratelimitBypass(request().get('/api/reset')); +const chai = require('chai'); +const expect = chai.expect; const compareObjectProps = (a, b) => { const missing = []; diff --git a/test/helpers/checks.js b/test/helpers/checks.js index 257a5e1..5d8bf4c 100644 --- a/test/helpers/checks.js +++ b/test/helpers/checks.js @@ -1,5 +1,6 @@ const { expect } = require('chai'); const locale = require('../../src/Util/i18n').__; +const { resetRatelimits } = require('./ratelimits'); const ratelimit = (context, limit, test, done, status = 200) => { context.retries(0); diff --git a/test/helpers/ratelimits.js b/test/helpers/ratelimits.js new file mode 100644 index 0000000..14a2ed0 --- /dev/null +++ b/test/helpers/ratelimits.js @@ -0,0 +1,7 @@ +const config = require('../../config'); +const request = require('./request'); + +const ratelimitBypass = (req) => req.set('X-Ratelimit-Bypass', config.secret); +const resetRatelimits = () => ratelimitBypass(request().get('/api/reset')); + +module.exports = { ratelimitBypass, resetRatelimits }; From bacd6e0e01475b27f7be15d0ed33bdc5b6edcda0 Mon Sep 17 00:00:00 2001 From: MattIPv4 Date: Sat, 18 Jan 2020 13:00:23 +0000 Subject: [PATCH 5/7] Test against DOM --- package.json | 1 + test/Routes/Lists.js | 66 +++++++++++++++++++++++++++-------------- test/base.js | 6 +++- test/helpers/dom.js | 6 ++++ test/helpers/request.js | 2 -- 5 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 test/helpers/dom.js diff --git a/package.json b/package.json index 488ed6d..cbbee0b 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "express": "^4.17.1", "express-minify": "^1.0.0", "i18n": "^0.8.3", + "jsdom": "^16.0.0", "kill-port": "^1.6.0", "knex": "^0.19.4", "lighthouse": "^5.2.0", diff --git a/test/Routes/Lists.js b/test/Routes/Lists.js index 7867199..1a37113 100644 --- a/test/Routes/Lists.js +++ b/test/Routes/Lists.js @@ -1,38 +1,60 @@ -const { describe, it, expect, request, db, locale, checks, auth } = require('../base'); +const { describe, it, expect, request, db, locale, checks, auth, dom } = require('../base'); describe('/lists', () => { describe('GET', () => { - const test = () => request().get('/lists'); - it('returns an OK status code', done => { - test().end((err, res) => { - expect(res).to.have.status(200); + let res, page; + before('fetch the page', done => { + request().get('/lists').end((_, r) => { + res = r; + page = dom(r); done(); }); }); + it('returns an OK status code', done => { + expect(res).to.have.status(200); + done(); + }); it('has the correct page title', done => { - test().end((err, res) => { - expect(res).to.be.html; - checks.title(res, `All Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); - done(); - }); + checks.title(res, `All Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); + done(); }); - it('renders the expected content', done => { - db.select('name', 'url').from('lists').where({ display: true, defunct: false }).then(lists => { - test().end((err, res) => { - expect(res).to.be.html; - // Confirm header - expect(res.text).to.include('All Bot Lists'); - - // Confirm footer stats - expect(res.text).to.include(`${locale('site_name')} - Bot List Stats`); + describe('renders the expected content', () => { + it('has the correct title', done => { + expect(res.text).to.include('All Bot Lists'); + done(); + }); + it('has the stats footer', done => { + const footer = page.querySelector(".hero.card .hero-body.hero-stats.card-body"); + expect(footer).to.exist; + expect(footer.innerHTML).to.include(`${locale('site_name')} - Bot List Stats`); + done(); + }); - // Confirm list cards - lists.forEach(list => { + describe('contains the list cards', () => { + let listCards; + before('fetch list cards', done => { + db.select('id', 'name', 'url').from('lists').where({ display: true, defunct: false }).then(lists => { + listCards = lists; + done(); + }); + }); + it('has the list names', done => { + listCards.forEach(list => { expect(res.text).to.include(list.name); + }); + done(); + }); + it('has the list urls', done => { + listCards.forEach(list => { expect(res.text).to.include(list.url); }); - + done(); + }); + it('has the list information button', done => { + listCards.forEach(list => { + expect(page.querySelector(`.card a.button[href="/lists/${list.id}"]`)).to.exist; + }); done(); }); }); diff --git a/test/base.js b/test/base.js index 9ac3647..d00d243 100644 --- a/test/base.js +++ b/test/base.js @@ -5,9 +5,12 @@ const checks = require('./helpers/checks'); const auth = require('./helpers/auth'); const request = require('./helpers/request'); const { ratelimitBypass, resetRatelimits } = require('./helpers/ratelimits'); +const dom = require('./helpers/dom'); const { describe, it } = require('mocha'); const chai = require('chai'); -const expect = chai.expect; +const { expect } = require('chai'); +const chaiHttp = require('chai-http'); +chai.use(chaiHttp); const compareObjectProps = (a, b) => { const missing = []; @@ -43,5 +46,6 @@ module.exports = { locale, checks, auth, + dom, compareObjects }; diff --git a/test/helpers/dom.js b/test/helpers/dom.js new file mode 100644 index 0000000..e9ee13c --- /dev/null +++ b/test/helpers/dom.js @@ -0,0 +1,6 @@ +const jsdom = require("jsdom"); +const { JSDOM } = jsdom; + +module.exports = res => { + return (new JSDOM(res.text)).window.document; +}; diff --git a/test/helpers/request.js b/test/helpers/request.js index ab9ee46..8161728 100644 --- a/test/helpers/request.js +++ b/test/helpers/request.js @@ -1,8 +1,6 @@ const config = require('../../config'); const { asAnon } = require('./auth'); const chai = require('chai'); -const chaiHttp = require('chai-http'); -chai.use(chaiHttp); const target = `${config.baseURL}:${config.port}`; const base = () => chai.request(target); From 8ccef5b598f163b96c054f9b6df31ae1d0a546cc Mon Sep 17 00:00:00 2001 From: MattIPv4 Date: Sat, 18 Jan 2020 21:58:54 +0000 Subject: [PATCH 6/7] Refactor /lists tests with shared checks --- test/Routes/Lists.js | 59 ++++------------- test/base.js | 5 +- test/helpers/checks.js | 131 +++++++++++++++++++++++++++++++++++++- test/helpers/fetchpage.js | 12 ++++ test/mocha.opts | 1 + 5 files changed, 160 insertions(+), 48 deletions(-) create mode 100644 test/helpers/fetchpage.js diff --git a/test/Routes/Lists.js b/test/Routes/Lists.js index 1a37113..835ce45 100644 --- a/test/Routes/Lists.js +++ b/test/Routes/Lists.js @@ -1,63 +1,30 @@ -const { describe, it, expect, request, db, locale, checks, auth, dom } = require('../base'); +const { describe, it, expect, request, db, locale, checks, auth, fetchPage } = require('../base'); describe('/lists', () => { describe('GET', () => { - let res, page; - before('fetch the page', done => { - request().get('/lists').end((_, r) => { - res = r; - page = dom(r); - done(); - }); - }); - it('returns an OK status code', done => { - expect(res).to.have.status(200); - done(); - }); - it('has the correct page title', done => { - checks.title(res, `All Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); + const test = () => request().get('/lists'); + fetchPage(test); + + it('returns an OK status code', function(done) { + expect(this.res).to.have.status(200); done(); }); + checks.meta(`All Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); + describe('renders the expected content', () => { - it('has the correct title', done => { - expect(res.text).to.include('All Bot Lists'); + it('has the correct title', function(done) { + expect(this.res.text).to.include('All Bot Lists'); done(); }); - it('has the stats footer', done => { - const footer = page.querySelector(".hero.card .hero-body.hero-stats.card-body"); + it('has the stats footer', function(done) { + const footer = this.page.querySelector(".hero.card .hero-body.hero-stats.card-body"); expect(footer).to.exist; expect(footer.innerHTML).to.include(`${locale('site_name')} - Bot List Stats`); done(); }); - describe('contains the list cards', () => { - let listCards; - before('fetch list cards', done => { - db.select('id', 'name', 'url').from('lists').where({ display: true, defunct: false }).then(lists => { - listCards = lists; - done(); - }); - }); - it('has the list names', done => { - listCards.forEach(list => { - expect(res.text).to.include(list.name); - }); - done(); - }); - it('has the list urls', done => { - listCards.forEach(list => { - expect(res.text).to.include(list.url); - }); - done(); - }); - it('has the list information button', done => { - listCards.forEach(list => { - expect(page.querySelector(`.card a.button[href="/lists/${list.id}"]`)).to.exist; - }); - done(); - }); - }); + checks.listCards(test, db, { display: true, defunct: false }); }); }); }); diff --git a/test/base.js b/test/base.js index d00d243..b8cd13d 100644 --- a/test/base.js +++ b/test/base.js @@ -6,7 +6,8 @@ const auth = require('./helpers/auth'); const request = require('./helpers/request'); const { ratelimitBypass, resetRatelimits } = require('./helpers/ratelimits'); const dom = require('./helpers/dom'); -const { describe, it } = require('mocha'); +const fetchPage = require('./helpers/fetchPage'); +const { before, describe, it } = require('mocha'); const chai = require('chai'); const { expect } = require('chai'); const chaiHttp = require('chai-http'); @@ -35,6 +36,7 @@ const compareObjects = (template, actual) => { }; module.exports = { + before, describe, it, expect, @@ -47,5 +49,6 @@ module.exports = { checks, auth, dom, + fetchPage, compareObjects }; diff --git a/test/helpers/checks.js b/test/helpers/checks.js index 5d8bf4c..b7e1b2f 100644 --- a/test/helpers/checks.js +++ b/test/helpers/checks.js @@ -1,6 +1,9 @@ +const { describe, it } = require('mocha'); const { expect } = require('chai'); const locale = require('../../src/Util/i18n').__; const { resetRatelimits } = require('./ratelimits'); +const fetchPage = require('./fetchPage'); +const auth = require('./auth'); const ratelimit = (context, limit, test, done, status = 200) => { context.retries(0); @@ -44,4 +47,130 @@ const title = (res, expectedTitle) => { expect(res.text).to.include(``); }; -module.exports = { ratelimit, authRequired, title }; +const meta = expectedTitle => { + describe('has the correct page metadata', () => { + it('is a valid HTML page', function (done) { + expect(this.res).to.be.html; + done(); + }); + it('has the correct title', function (done) { + const title = this.page.querySelector('title'); + expect(title).to.exist; + expect(title.textContent).to.eq(expectedTitle); + done(); + }); + it('has the correct description', function (done) { + const description = this.page.querySelector('meta[name="description"]'); + expect(description).to.exist; + expect(description.hasAttribute('content')).to.be.true; + expect(description.getAttribute('content')).to.eq(`${locale('site_name')} - ${locale('full_desc')}`); + done(); + }); + describe('OpenGraph', () => { + it('has the correct title', function (done) { + const title = this.page.querySelector('meta[property="og:title"]'); + expect(title).to.exist; + expect(title.hasAttribute('content')).to.be.true; + expect(title.getAttribute('content')).to.eq(expectedTitle); + done(); + }); + it('has the correct site name', function (done) { + const title = this.page.querySelector('meta[property="og:site_name"]'); + expect(title).to.exist; + expect(title.hasAttribute('content')).to.be.true; + expect(title.getAttribute('content')).to.eq(locale('site_name')); + done(); + }); + it('has the correct description', function (done) { + const description = this.page.querySelector('meta[property="og:description"]'); + expect(description).to.exist; + expect(description.hasAttribute('content')).to.be.true; + expect(description.getAttribute('content')).to.eq(locale('full_desc')); + done(); + }); + }); + describe('Twitter', () => { + it('has the correct title', function (done) { + const title = this.page.querySelector('meta[name="twitter:title"]'); + expect(title).to.exist; + expect(title.hasAttribute('content')).to.be.true; + expect(title.getAttribute('content')).to.eq(expectedTitle); + done(); + }); + it('has the correct description', function (done) { + const description = this.page.querySelector('meta[name="twitter:description"]'); + expect(description).to.exist; + expect(description.hasAttribute('content')).to.be.true; + expect(description.getAttribute('content')).to.eq(locale('full_desc')); + done(); + }); + }); + }); +}; + +const listCards = (test, db, where) => { + describe('contains the list cards', () => { + fetchPage(test); + + before('fetch list cards', function(done) { + db.select('id', 'name', 'url').from('lists').where(where).then(lists => { + this.listCards = lists; + done(); + }); + }); + + it('has the list names', function(done) { + this.listCards.forEach(list => { + expect(this.res.text).to.include(list.name); + }); + done(); + }); + it('has the list urls', function(done) { + this.listCards.forEach(list => { + expect(this.res.text).to.include(list.url); + }); + done(); + }); + + describe('card buttons', () => { + it('has the list information button', function(done) { + this.listCards.forEach(list => { + expect(this.page.querySelector(`.card a.button[href="/lists/${list.id}"]`)).to.exist; + }); + done(); + }); + + describe('as an anonymous user', () => { + fetchPage(() => auth.asAnon(test())); + it('does not have the edit button', function(done) { + this.listCards.forEach(list => { + expect(this.page.querySelector(`.card a.button[href="/lists/${list.id}/edit"]`)).to.not.exist; + }); + done(); + }); + }); + + describe('as a logged in user', () => { + fetchPage(() => auth.asUser(test())); + it('does not have the edit button', function(done) { + this.listCards.forEach(list => { + expect(this.page.querySelector(`.card a.button[href="/lists/${list.id}/edit"]`)).to.not.exist; + }); + done(); + }); + }); + + describe('as a moderator', () => { + fetchPage(() => auth.asMod(test())); + it('has the edit button', function(done) { + this.listCards.forEach(list => { + expect(this.page.querySelector(`.card a.button[href="/lists/${list.id}/edit"]`)).to.exist; + }); + done(); + }); + }); + }); + }); +}; + +module.exports = { ratelimit, authRequired, title, meta, listCards }; diff --git a/test/helpers/fetchpage.js b/test/helpers/fetchpage.js new file mode 100644 index 0000000..788f511 --- /dev/null +++ b/test/helpers/fetchpage.js @@ -0,0 +1,12 @@ +const { before } = require('mocha'); +const dom = require('./dom'); + +module.exports = test => { + before('fetch the page', function(done) { + test().end((_, r) => { + this.res = r; + this.page = dom(r); + done(); + }); + }); +}; diff --git a/test/mocha.opts b/test/mocha.opts index 003d848..3080ec3 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,6 +1,7 @@ # mocha.opts --recursive --exclude base.js + --exclude helpers --slow 600 --timeout 12000 --retries 1 From e4a3ba17a685cb0afac237c0f1891c1d1505ea48 Mon Sep 17 00:00:00 2001 From: MattIPv4 Date: Fri, 6 Mar 2020 12:42:34 +0000 Subject: [PATCH 7/7] Add a load more auth tests to Authentication.js & Index.js --- src/Routes/Authentication.js | 1 - src/Routes/Index.js | 1 - test/Routes/API.js | 188 ++++++++++---------- test/Routes/Authentication.js | 46 ++++- test/Routes/Index.js | 313 +++++++++++++++++++++++++++++++++- test/Routes/Lists.js | 2 +- test/helpers/auth.js | 13 +- test/helpers/request.js | 10 +- 8 files changed, 463 insertions(+), 111 deletions(-) diff --git a/src/Routes/Authentication.js b/src/Routes/Authentication.js index 36adb0c..d9c0b33 100644 --- a/src/Routes/Authentication.js +++ b/src/Routes/Authentication.js @@ -68,7 +68,6 @@ class AuthenticationRoute extends BaseRoute { req.session = null; res.redirect('/'); }); - } get getRouter() { diff --git a/src/Routes/Index.js b/src/Routes/Index.js index fce7607..74a58a6 100644 --- a/src/Routes/Index.js +++ b/src/Routes/Index.js @@ -133,7 +133,6 @@ class IndexRoute extends BaseRoute { }); }); - this.router.get('/sitemap', (req, res) => { sitemap.get(this.db).then(data => { sitemap.save(data).then(() => { diff --git a/test/Routes/API.js b/test/Routes/API.js index 658d45d..da8c483 100644 --- a/test/Routes/API.js +++ b/test/Routes/API.js @@ -535,6 +535,100 @@ describe('/api/lists/:id', () => { }); }); +describe('/api/legacy-ids', () => { + describe('GET', () => { + const test = () => ratelimitBypass(request().get('/api/legacy-ids')); + it('returns an OK status code', done => { + test().end((err, res) => { + expect(res).to.have.status(200); + done(); + }); + }); + it('has a permissive CORS header', done => { + test().end((err, res) => { + expect(res).to.have.header('Access-Control-Allow-Origin', '*'); + done(); + }); + }); + it('returns a valid JSON body', done => { + test().end((err, res) => { + expect(res).to.be.json; + done(); + }); + }); + it('contains an object of strings', done => { + test().end((err, res) => { + expect(res.body).to.be.a('object'); + const entries = Object.values(res.body); + entries.forEach(entry => { + expect(entry).to.be.a('string'); + }); + done(); + }); + }); + }); + + describe('GET (Ratelimited)', () => { + const test = () => request().get('/api/legacy-ids'); + it('ratelimits spam requests', done => { + resetRatelimits().end(() => { + test().end(() => { + }); + setTimeout(() => { + test().end((err, res) => { + expect(res).to.have.status(429); + expect(res).to.be.json; + + expect(res.body).to.have.property('error', true); + expect(res.body).to.have.property('status', 429); + + expect(res.body).to.have.property('retry_after'); + expect(res.body.retry_after).to.be.a('number'); + + expect(res.body).to.have.property('ratelimit_reset'); + expect(res.body.ratelimit_reset).to.be.a('number'); + + expect(res.body).to.have.property('ratelimit_ip'); + expect(res.body.ratelimit_ip).to.be.a('string'); + + expect(res.body).to.have.property('ratelimit_route', '/api/legacy-ids'); + expect(res.body).to.have.property('ratelimit_bot_id', ''); + done(); + }); + }, 200); + }); + }); + it('does not ratelimit requests spaced correctly', function (done) { + checks.ratelimit(this, 1, test, done); + }); + }); + + describe('POST', () => { + const test = () => ratelimitBypass(request().post('/api/legacy-ids')); + it('returns a Not Found status code', done => { + test().end((err, res) => { + expect(res).to.have.status(404); + done(); + }); + }); + it('has a permissive CORS header', done => { + test().end((err, res) => { + expect(res).to.have.header('Access-Control-Allow-Origin', '*'); + done(); + }); + }); + it('returns an error JSON body', done => { + test().end((err, res) => { + expect(res).to.be.json; + expect(res.body).to.have.property('error', true); + expect(res.body).to.have.property('status', 404); + expect(res.body).to.have.property('message', 'Endpoint not found'); + done(); + }); + }); + }); +}); + describe('/api/count', () => { describe('GET', () => { const test = () => ratelimitBypass(request().get('/api/count')); @@ -1332,97 +1426,3 @@ describe('/api/bots/:id', () => { }); }); }); - -describe('/api/legacy-ids', () => { - describe('GET', () => { - const test = () => ratelimitBypass(request().get('/api/legacy-ids')); - it('returns an OK status code', done => { - test().end((err, res) => { - expect(res).to.have.status(200); - done(); - }); - }); - it('has a permissive CORS header', done => { - test().end((err, res) => { - expect(res).to.have.header('Access-Control-Allow-Origin', '*'); - done(); - }); - }); - it('returns a valid JSON body', done => { - test().end((err, res) => { - expect(res).to.be.json; - done(); - }); - }); - it('contains an object of strings', done => { - test().end((err, res) => { - expect(res.body).to.be.a('object'); - const entries = Object.values(res.body); - entries.forEach(entry => { - expect(entry).to.be.a('string'); - }); - done(); - }); - }); - }); - - describe('GET (Ratelimited)', () => { - const test = () => request().get('/api/legacy-ids'); - it('ratelimits spam requests', done => { - resetRatelimits().end(() => { - test().end(() => { - }); - setTimeout(() => { - test().end((err, res) => { - expect(res).to.have.status(429); - expect(res).to.be.json; - - expect(res.body).to.have.property('error', true); - expect(res.body).to.have.property('status', 429); - - expect(res.body).to.have.property('retry_after'); - expect(res.body.retry_after).to.be.a('number'); - - expect(res.body).to.have.property('ratelimit_reset'); - expect(res.body.ratelimit_reset).to.be.a('number'); - - expect(res.body).to.have.property('ratelimit_ip'); - expect(res.body.ratelimit_ip).to.be.a('string'); - - expect(res.body).to.have.property('ratelimit_route', '/api/legacy-ids'); - expect(res.body).to.have.property('ratelimit_bot_id', ''); - done(); - }); - }, 200); - }); - }); - it('does not ratelimit requests spaced correctly', function (done) { - checks.ratelimit(this, 1, test, done); - }); - }); - - describe('POST', () => { - const test = () => ratelimitBypass(request().post('/api/legacy-ids')); - it('returns a Not Found status code', done => { - test().end((err, res) => { - expect(res).to.have.status(404); - done(); - }); - }); - it('has a permissive CORS header', done => { - test().end((err, res) => { - expect(res).to.have.header('Access-Control-Allow-Origin', '*'); - done(); - }); - }); - it('returns an error JSON body', done => { - test().end((err, res) => { - expect(res).to.be.json; - expect(res.body).to.have.property('error', true); - expect(res.body).to.have.property('status', 404); - expect(res.body).to.have.property('message', 'Endpoint not found'); - done(); - }); - }); - }); -}); diff --git a/test/Routes/Authentication.js b/test/Routes/Authentication.js index 6ae36d2..d252abf 100644 --- a/test/Routes/Authentication.js +++ b/test/Routes/Authentication.js @@ -1,4 +1,4 @@ -const { describe, it, expect, request } = require('../base'); +const { describe, it, expect, request, auth } = require('../base'); describe('/auth', () => { describe('GET', () => { @@ -24,11 +24,45 @@ describe('/auth', () => { describe('/auth/logout', () => { describe('GET', () => { - const test = () => request().get('/auth/logout').redirects(0); - it('redirects to back to the homepage', done => { - test().end((err, res) => { - expect(res).to.redirectTo('/'); - done(); + describe('As an anonymous user', () => { + it('redirects to back to the homepage', done => { + auth.asAnon(request().get('/')).end((err1, res1) => { + expect(res1.text).to.include('Sign in with Discord'); + + auth.asPrevious(request().get('/')).end((err2, res2) => { + expect(res2.text).to.include('Sign in with Discord'); + + auth.asPrevious(request().get('/auth/logout')).redirects(0).end((err3, res3) => { + expect(res3).to.redirectTo('/'); + + auth.asPrevious(request().get('/')).end((err4, res4) => { + expect(res4.text).to.include('Sign in with Discord'); + done(); + }); + }); + }); + }); + }); + }); + + describe('As a logged in user', () => { + it('redirects to back to the homepage', done => { + auth.asUser(request().get('/')).end((err1, res1) => { + expect(res1.text).to.include(''); + + auth.asPrevious(request().get('/')).end((err2, res2) => { + expect(res2.text).to.include(''); + + auth.asPrevious(request().get('/auth/logout')).redirects(0).end((err3, res3) => { + expect(res3).to.redirectTo('/'); + + auth.asPrevious(request().get('/')).end((err4, res4) => { + expect(res4.text).to.include('Sign in with Discord'); + done(); + }); + }); + }); + }); }); }); }); diff --git a/test/Routes/Index.js b/test/Routes/Index.js index f5d636b..0528f68 100644 --- a/test/Routes/Index.js +++ b/test/Routes/Index.js @@ -1,4 +1,4 @@ -const { describe, it, expect, request, db, locale, checks } = require('../base'); +const { describe, it, expect, request, db, locale, checks, auth } = require('../base'); const renderer = new (require('../../src/Structure/Markdown'))(); describe('/', () => { @@ -99,6 +99,317 @@ describe('/about', () => { }); }); +describe('/about/manage', () => { + describe('GET', () => { + describe('As an anonymous user', () => { + const test = () => request().get('/about/manage'); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + + describe('As a logged in user', () => { + const test = () => auth.asUser(request().get('/about/manage')); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + + describe('As a moderator', () => { + const test = () => auth.asMod(request().get('/about/manage')); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + + describe('As an administrator', () => { + const test = () => auth.asAdmin(request().get('/about/manage')); + it('returns an OK status code', done => { + test().end((err, res) => { + expect(res).to.have.status(200); + done(); + }); + }); + it('has the correct page title', done => { + test().end((err, res) => { + expect(res).to.be.html; + checks.title(res, `About - ${locale('site_name')} - ${locale('short_desc')}`); + done(); + }); + }); + it('renders the expected information', done => { + db.select('id', 'title').from('about').then(sections => { + test().end((err, res) => { + expect(res).to.be.html; + + // Confirm header + expect(res.text).to.include(`About Manager`); + + // Confirm sections + sections.forEach(section => { + expect(res.text).to.include(section.name); + expect(res.text).to.include(`href="/about/manage/${section.id}"`); + }); + + done(); + }); + }); + }); + }); + }); +}); + +describe('/about/manage/add', () => { + describe('As an anonymous user', () => { + describe('GET', () => { + const test = () => request().get('/about/manage/add'); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + + describe('POST', () => { + const test = () => request().post('/about/manage/add'); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + }); + + describe('As a logged in user', () => { + describe('GET', () => { + const test = () => auth.asUser(request().get('/about/manage/add')); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + + describe('POST', () => { + const test = () => auth.asUser(request().post('/about/manage/add')); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + }); + + describe('As a moderator', () => { + describe('GET', () => { + const test = () => auth.asMod(request().get('/about/manage/add')); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + + describe('POST', () => { + const test = () => auth.asMod(request().post('/about/manage/add')); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + }); + + describe('As an administrator', () => { + describe('GET', () => { + const test = () => auth.asAdmin(request().get('/about/manage/add')); + it('returns an OK status code', done => { + test().end((err, res) => { + expect(res).to.have.status(200); + done(); + }); + }); + it('has the correct page title', done => { + test().end((err, res) => { + expect(res).to.be.html; + checks.title(res, `Add Section - ${locale('site_name')} - ${locale('short_desc')}`); + done(); + }); + }); + }); + }); +}); + +describe('/about/manage/:id', () => { + const sectionId = 'intro'; + describe('As an anonymous user', () => { + describe(`GET (:id = ${sectionId})`, () => { + const test = () => request().get(`/about/manage/${sectionId}`); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + + describe(`POST (:id = ${sectionId})`, () => { + const test = () => request().post(`/about/manage/${sectionId}`); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + }); + + describe('As a logged in user', () => { + describe(`GET (:id = ${sectionId})`, () => { + const test = () => auth.asUser(request().get(`/about/manage/${sectionId}`)); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + + describe(`POST (:id = ${sectionId})`, () => { + const test = () => auth.asUser(request().post(`/about/manage/${sectionId}`)); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + }); + + describe('As a moderator', () => { + describe(`GET (:id = ${sectionId})`, () => { + const test = () => auth.asMod(request().get(`/about/manage/${sectionId}`)); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + + describe(`POST (:id = ${sectionId})`, () => { + const test = () => auth.asMod(request().post(`/about/manage/${sectionId}`)); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + }); + + describe('As an administrator', () => { + describe(`GET (:id = ${sectionId})`, () => { + const test = () => auth.asAdmin(request().get(`/about/manage/${sectionId}`)); + it('returns an OK status code', done => { + test().end((err, res) => { + expect(res).to.have.status(200); + done(); + }); + }); + it('has the correct page title', done => { + test().end((err, res) => { + expect(res).to.be.html; + checks.title(res, `Edit Section - ${locale('site_name')} - ${locale('short_desc')}`); + done(); + }); + }); + }); + }); +}); + +describe('/about/manage/:id/delete', () => { + const sectionId = 'intro'; + describe('As an anonymous user', () => { + describe(`GET (:id = ${sectionId})`, () => { + const test = () => request().get(`/about/manage/${sectionId}/delete`); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + }); + + describe('As a logged in user', () => { + describe(`GET (:id = ${sectionId})`, () => { + const test = () => auth.asUser(request().get(`/about/manage/${sectionId}/delete`)); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + }); + + describe('As a moderator', () => { + describe(`GET (:id = ${sectionId})`, () => { + const test = () => auth.asMod(request().get(`/about/manage/${sectionId}/delete`)); + it('returns the authentication required message', done => { + test().end((err, res) => { + checks.authRequired(res); + done(); + }); + }); + }); + }); +}); + +describe('/sitemap', () => { + describe('GET', () => { + const test = () => request().get('/sitemap'); + it('returns an OK status code', done => { + test().end((err, res) => { + expect(res).to.have.status(200); + done(); + }); + }); + it('has the correct page title', done => { + test().end((err, res) => { + expect(res).to.be.html; + checks.title(res, `Sitemap - ${locale('site_name')} - ${locale('short_desc')}`); + done(); + }); + }); + it('renders the expected content', done => { + test().end((err, res) => { + expect(res).to.be.html; + + // Confirm button + expect(res.text).to.include('href="/sitemap.xml">View as XML'); + done(); + }); + }); + }); +}); + describe('Discord Invite', () => { const routes = [ '/invite', diff --git a/test/Routes/Lists.js b/test/Routes/Lists.js index 835ce45..8a32450 100644 --- a/test/Routes/Lists.js +++ b/test/Routes/Lists.js @@ -41,7 +41,7 @@ describe('/lists/best-practices', () => { it('has the correct page title', done => { test().end((err, res) => { expect(res).to.be.html; - titleCheck(res, `Best Practices for Discord Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); + checks.title(res, `Best Practices for Discord Bot Lists - ${locale('site_name')} - ${locale('short_desc')}`); done(); }); }); diff --git a/test/helpers/auth.js b/test/helpers/auth.js index 05d5a30..123350b 100644 --- a/test/helpers/auth.js +++ b/test/helpers/auth.js @@ -1,8 +1,11 @@ const config = require('../../config'); -const asAnon = (req) => req.set('X-Auth-As-Anon', config.secret); -const asUser = (req) => req.set('X-Auth-As-User', config.secret).unset('X-Auth-As-Anon'); -const asMod = (req) => req.set('X-Auth-As-Mod', config.secret).unset('X-Auth-As-Anon'); -const asAdmin = (req) => req.set('X-Auth-As-Admin', config.secret).unset('X-Auth-As-Anon'); +const clear = req => req.unset('X-Auth-As-Anon').unset('X-Auth-As-User').unset('X-Auth-As-Mod').unset('X-Auth-As-Admin'); -module.exports = { asAnon, asUser, asMod, asAdmin }; +const asAnon = req => clear(req).set('X-Auth-As-Anon', config.secret); +const asUser = req => clear(req).set('X-Auth-As-User', config.secret); +const asMod = req => clear(req).set('X-Auth-As-Mod', config.secret); +const asAdmin = req => clear(req).set('X-Auth-As-Admin', config.secret); +const asPrevious = req => clear(req); + +module.exports = { asAnon, asUser, asMod, asAdmin, asPrevious }; diff --git a/test/helpers/request.js b/test/helpers/request.js index 8161728..0196952 100644 --- a/test/helpers/request.js +++ b/test/helpers/request.js @@ -1,14 +1,20 @@ const config = require('../../config'); const { asAnon } = require('./auth'); const chai = require('chai'); +const chaiHttp = require('chai-http'); +chai.use(chaiHttp); const target = `${config.baseURL}:${config.port}`; -const base = () => chai.request(target); +let agent; +const base = () => { + if (!agent) agent = chai.request.agent(target); + return agent; +}; module.exports = () => new Proxy(base, { get(target, method) { return function (...args) { - return asAnon(target()[method].apply(this, args)); + return asAnon(target()[method].apply(agent, args)); }; } });