diff --git a/package.json b/package.json index 05872f9..9d3c8c2 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,13 @@ "@types/nunjucks": "^3.1.3", "@types/on-finished": "^2.3.1", "@types/redis": "^2.8.18", + "@types/supertest": "^2.0.10", "@types/uuid": "^8.0.0", "@types/ws": "^7.2.4", "jest": "^26.1.0", "maildev": "^1.1.0", "node-fetch": "^2.6.0", + "supertest": "^4.0.2", "ts-jest": "^26.1.1", "typescript": "^3.8.3" }, diff --git a/src/components/ExpressAppComponent.ts b/src/components/ExpressAppComponent.ts index 2ec5bd5..1ee1d3f 100644 --- a/src/components/ExpressAppComponent.ts +++ b/src/components/ExpressAppComponent.ts @@ -7,6 +7,7 @@ import compression from "compression"; export default class ExpressAppComponent extends ApplicationComponent { private readonly port: number; private server?: Server; + private expressApp?: Express; constructor(port: number) { super(); @@ -20,6 +21,8 @@ export default class ExpressAppComponent extends ApplicationComponent { // Proxy app.set('trust proxy', 'loopback'); + + this.expressApp = app; } public async init(router: Router): Promise { @@ -48,4 +51,8 @@ export default class ExpressAppComponent extends ApplicationComponent { if (!this.server) throw 'Server was not initialized.'; return this.server; } + + public getExpressApp(): Express { + return this.expressApp!; + } } diff --git a/test/CsrfProtectionComponent.test.ts b/test/CsrfProtectionComponent.test.ts index 70f539a..d6ab12d 100644 --- a/test/CsrfProtectionComponent.test.ts +++ b/test/CsrfProtectionComponent.test.ts @@ -1,10 +1,11 @@ -import fetch, {Response} from "node-fetch"; -import useApp, {DEFAULT_ADDR, TestApp} from "./_app"; +import useApp, {TestApp} from "./_app"; import Controller from "../src/Controller"; import CsrfProtectionComponent from "../src/components/CsrfProtectionComponent"; +import supertest from "supertest"; +let app: TestApp; useApp(port => { - return new class extends TestApp { + return app = new class extends TestApp { protected async init(): Promise { this.use(new class extends Controller { routes(): void { @@ -32,86 +33,68 @@ useApp(port => { }); describe('Test CSRF protection', () => { - let res: Response; + let cookies: string[]; + let csrf: string; - test('no csrf token should be in session at first', async () => { - res = await fetch(DEFAULT_ADDR, { - method: 'POST', - }); - expect(res.status).toBe(401); - expect(await res.text()).toContain(`You weren't assigned any CSRF token.`); + test('no csrf token should be in session at first', (done) => { + const agent = supertest(app.getExpressApp()); + agent.post('/') + .expect(401, (err, res) => { + if (err) return done(err); + + expect(res.text).toContain(`You weren't assigned any CSRF token.`); + cookies = res.get('Set-Cookie'); + + agent.get('/') + .set('Cookie', cookies) + .expect(200, (err, res) => { + if (err) return done(err); + + csrf = res.text; + done(); + }); + }); }); - let cookies: string | null; - - test('sending no csrf token should fail', async () => { - cookies = res.headers.get('set-cookie'); + test('sending no csrf token should fail', (done) => { expect(cookies).toBeDefined(); - res = await fetch(`${DEFAULT_ADDR}${Controller.route('csrf_test')}`, { - headers: { - 'Cookie': cookies!, - } - }); - expect(res.ok).toBe(true); + const agent = supertest(app.getExpressApp()); + agent.post('/') + .set('Cookie', cookies) + .expect(401) + .end((err, res) => { + if (err) return done(err); - res = await fetch(DEFAULT_ADDR, { - method: 'POST', - headers: { - 'Cookie': cookies!, - } - }); - expect(res.status).toBe(401); - expect(await res.text()).toContain(`You didn't provide any CSRF token.`); + expect(res.text).toContain(`You didn't provide any CSRF token.`); + done(); + }); }); - test('sending an invalid csrf token should fail', async () => { - cookies = res.headers.get('set-cookie'); + test('sending an invalid csrf token should fail', (done) => { expect(cookies).toBeDefined(); - res = await fetch(`${DEFAULT_ADDR}${Controller.route('csrf_test')}`, { - headers: { - 'Cookie': cookies!, - } - }); - expect(res.ok).toBe(true); + const agent = supertest(app.getExpressApp()); + agent.post('/') + .set('Cookie', cookies) + .set('Content-Type', 'application/json') + .send({csrf: 'not_a_valid_csrf'}) + .expect(401, (err, res) => { + if (err) return done(err); - res = await fetch(DEFAULT_ADDR, { - method: 'POST', - body: JSON.stringify({ - csrf: 'not_a_valid_csrf', - }), - headers: { - 'Content-Type': 'application/json', - 'Cookie': cookies!, - } - }); - expect(res.status).toBe(401); - expect(await res.text()).toContain(`Tokens don't match.`); + expect(res.text).toContain(`Tokens don't match.`); + done(); + }); }); - test('sending a valid csrf token should success', async () => { - cookies = res.headers.get('set-cookie'); + test('sending a valid csrf token should success', (done) => { expect(cookies).toBeDefined(); - res = await fetch(`${DEFAULT_ADDR}${Controller.route('csrf_test')}`, { - headers: { - 'Cookie': cookies!, - } - }); - expect(res.ok).toBe(true); - let csrf = await res.text(); - - res = await fetch(DEFAULT_ADDR, { - method: 'POST', - body: JSON.stringify({ - csrf: csrf, - }), - headers: { - 'Content-Type': 'application/json', - 'Cookie': cookies!, - } - }); - expect(res.ok).toBe(true); + const agent = supertest(app.getExpressApp()); + agent.post('/') + .set('Cookie', cookies) + .set('Content-Type', 'application/json') + .send({csrf: csrf}) + .expect(200, done); }); }); \ No newline at end of file diff --git a/test/Model.test.ts b/test/Model.test.ts index c77a830..2c770a9 100644 --- a/test/Model.test.ts +++ b/test/Model.test.ts @@ -2,7 +2,7 @@ import MysqlConnectionManager from "../src/db/MysqlConnectionManager"; import Model from "../src/db/Model"; import {MIGRATIONS} from "./_migrations"; import ModelFactory from "../src/db/ModelFactory"; -import {ValidationBag, ValidationError} from "../src/db/Validator"; +import {ValidationBag} from "../src/db/Validator"; class FakeDummyModel extends Model { public id?: number = undefined; diff --git a/test/_app.ts b/test/_app.ts index d2caf87..5b23778 100644 --- a/test/_app.ts +++ b/test/_app.ts @@ -16,6 +16,7 @@ import MagicLink from "../src/auth/models/MagicLink"; import FormHelperComponent from "../src/components/FormHelperComponent"; import RedirectBackComponent from "../src/components/RedirectBackComponent"; import ServeStaticDirectoryComponent from "../src/components/ServeStaticDirectoryComponent"; +import {Express} from "express"; export default function useApp(appSupplier?: (port: number) => TestApp) { let app: Application; @@ -37,6 +38,7 @@ export default function useApp(appSupplier?: (port: number) => TestApp) { export class TestApp extends Application { private readonly port: number; + private expressAppComponent?: ExpressAppComponent; constructor(port: number) { super(require('../package.json').version, true); @@ -54,12 +56,12 @@ export class TestApp extends Application { } protected registerComponents() { - const expressAppComponent = new ExpressAppComponent(this.port); + this.expressAppComponent = new ExpressAppComponent(this.port); const redisComponent = new RedisComponent(); const mysqlComponent = new MysqlComponent(); // Base - this.use(expressAppComponent); + this.use(this.expressAppComponent); this.use(new LogRequestsComponent()); // Static files @@ -93,6 +95,10 @@ export class TestApp extends Application { protected registerControllers() { } + + public getExpressApp(): Express { + return this.expressAppComponent!.getExpressApp(); + } } export const DEFAULT_ADDR = 'http://localhost:8966'; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index c886470..8ddba33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -585,6 +585,11 @@ resolved "https://registry.toot.party/@types%2fcookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg== +"@types/cookiejar@*": + version "2.1.1" + resolved "https://registry.toot.party/@types%2fcookiejar/-/cookiejar-2.1.1.tgz#90b68446364baf9efd8e8349bb36bd3852b75b80" + integrity sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw== + "@types/events@*": version "3.0.0" resolved "https://registry.toot.party/@types%2fevents/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" @@ -761,6 +766,21 @@ resolved "https://registry.toot.party/@types%2fstack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/superagent@*": + version "4.1.8" + resolved "https://registry.toot.party/@types%2fsuperagent/-/superagent-4.1.8.tgz#f663dcdd24705e07fce055003ace9b824f2a51c9" + integrity sha512-iol9KxQ7SLHatBJUiZ4uABrS4VS1frLjqPednxZz82eoCzo3Uy3TOH0p0ZIBbfBj8E/xqOtvizjBs9h7xi/l2g== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.10": + version "2.0.10" + resolved "https://registry.toot.party/@types%2fsupertest/-/supertest-2.0.10.tgz#630d79b4d82c73e043e43ff777a9ca98d457cab7" + integrity sha512-Xt8TbEyZTnD5Xulw95GLMOkmjGICrOQyJ2jqgkSjAUR3mm7pAIzSR0NFBaMcwlzVvlpCjNwbATcWWwjNiZiFrQ== + dependencies: + "@types/superagent" "*" + "@types/uuid@^8.0.0": version "8.0.0" resolved "https://registry.toot.party/@types%2fuuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0" @@ -1509,7 +1529,7 @@ component-emitter@1.2.1: resolved "https://registry.toot.party/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= -component-emitter@^1.2.1: +component-emitter@^1.2.0, component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.toot.party/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -1621,6 +1641,11 @@ cookie@^0.4.1: resolved "https://registry.toot.party/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +cookiejar@^2.1.0: + version "2.1.2" + resolved "https://registry.toot.party/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.toot.party/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -1727,7 +1752,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@^3.2.6: +debug@^3.1.0, debug@^3.2.6: version "3.2.6" resolved "https://registry.toot.party/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -2204,7 +2229,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@~3.0.0, extend@~3.0.2: +extend@^3.0.0, extend@~3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.toot.party/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -2317,6 +2342,15 @@ forever-agent@~0.6.1: resolved "https://registry.toot.party/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^2.3.1: + version "2.5.1" + resolved "https://registry.toot.party/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + form-data@^3.0.0: version "3.0.0" resolved "https://registry.toot.party/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" @@ -2335,7 +2369,7 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -formidable@^1.2.2: +formidable@^1.2.0, formidable@^1.2.2: version "1.2.2" resolved "https://registry.toot.party/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== @@ -3715,7 +3749,7 @@ merge-stream@^2.0.0: resolved "https://registry.toot.party/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -methods@~1.1.2: +methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.toot.party/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -3759,7 +3793,7 @@ mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: dependencies: mime-db "1.44.0" -mime@1.6.0, mime@^1.6.0: +mime@1.6.0, mime@^1.4.1, mime@^1.6.0: version "1.6.0" resolved "https://registry.toot.party/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -4745,6 +4779,11 @@ qs@6.7.0: resolved "https://registry.toot.party/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.5.1: + version "6.9.4" + resolved "https://registry.toot.party/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.toot.party/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -4804,7 +4843,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@2.3.7, readable-stream@^2.0.6: +readable-stream@2.3.7, readable-stream@^2.0.6, readable-stream@^2.3.5: version "2.3.7" resolved "https://registry.toot.party/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -5494,6 +5533,30 @@ strip-json-comments@~2.0.1: resolved "https://registry.toot.party/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +superagent@^3.8.3: + version "3.8.3" + resolved "https://registry.toot.party/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" + integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.2.0" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.3.5" + +supertest@^4.0.2: + version "4.0.2" + resolved "https://registry.toot.party/supertest/-/supertest-4.0.2.tgz#c2234dbdd6dc79b6f15b99c8d6577b90e4ce3f36" + integrity sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ== + dependencies: + methods "^1.1.2" + superagent "^3.8.3" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.toot.party/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"