diff --git a/test/Authentication.test.ts b/test/Authentication.test.ts index 1a5e038..a9a0220 100644 --- a/test/Authentication.test.ts +++ b/test/Authentication.test.ts @@ -3,20 +3,15 @@ import useApp from "./_app"; import Controller from "../src/Controller"; import supertest from "supertest"; import CsrfProtectionComponent from "../src/components/CsrfProtectionComponent"; -import MysqlConnectionManager from "../src/db/MysqlConnectionManager"; -import config from "config"; import User from "../src/auth/models/User"; import UserNameComponent from "../src/auth/models/UserNameComponent"; import UserPasswordComponent from "../src/auth/password/UserPasswordComponent"; import {popEmail} from "./_mail_server"; import AuthComponent from "../src/auth/AuthComponent"; +import {followMagicLinkFromMail, testLogout} from "./_authentication_common"; let app: TestApp; useApp(async (addr, port) => { - await MysqlConnectionManager.prepare(); - await MysqlConnectionManager.query('DROP DATABASE IF EXISTS ' + config.get('mysql.database')); - await MysqlConnectionManager.endPool(); - return app = new class extends TestApp { protected async init(): Promise { this.use(new class extends Controller { @@ -40,35 +35,6 @@ useApp(async (addr, port) => { }(addr, port); }); -async function followMagicLinkFromMail(cookies: string[]): Promise { - const mail: Record | null = await popEmail(); - expect(mail).not.toBeNull(); - - const query = (mail?.text as string).split('/magic/link?')[1].split('\n')[0]; - expect(query).toBeDefined(); - - await agent.get('/magic/link?' + query) - .expect(200); - await agent.get('/magic/lobby') - .set('Cookie', cookies) - .expect(302) - .expect('Location', '/'); -} - -async function testLogout(cookies: string[], csrf: string): Promise { - // Authenticated - await agent.get('/is-auth').set('Cookie', cookies).expect(200); - - // Logout - await agent.post('/auth/logout') - .set('Cookie', cookies) - .send({csrf: csrf}) - .expect(302); - - // Not authenticated - await agent.get('/is-auth').set('Cookie', cookies).expect(401); -} - let agent: supertest.SuperTest; beforeAll(() => { @@ -98,7 +64,6 @@ describe('Register with username and password (password)', () => { .expect(302) .expect('Location', '/'); - // Verify saved user const user = await User.select() .where('name', 'entrapta') @@ -129,6 +94,10 @@ describe('Register with username and password (password)', () => { expect(user2).toBeNull(); }); + test('Logout', async () => { + await testLogout(agent, cookies, csrf); + }); + test('Cannot register taken username', async () => { // Check that there is no hordak in DB expect(await User.select() @@ -136,12 +105,14 @@ describe('Register with username and password (password)', () => { .count()).toStrictEqual(0); const res1 = await agent.get('/csrf').expect(200); + const cookies = res1.get('Set-Cookie'); + const csrf = res1.text; // Register user await agent.post('/auth/register') - .set('Cookie', res1.get('Set-Cookie')) + .set('Cookie', cookies) .send({ - csrf: res1.text, + csrf: csrf, auth_method: 'password', identifier: 'hordak', password: 'horde_prime_will_rise', @@ -150,6 +121,7 @@ describe('Register with username and password (password)', () => { }) .expect(302) .expect('Location', '/'); + await testLogout(agent, cookies, csrf); // Verify saved user expect(await User.select() @@ -157,13 +129,11 @@ describe('Register with username and password (password)', () => { .count()).toStrictEqual(1); - const res2 = await agent.get('/csrf').expect(200); - // Attempt register same user const res = await agent.post('/auth/register') - .set('Cookie', res2.get('Set-Cookie')) + .set('Cookie', cookies) .send({ - csrf: res2.text, + csrf: csrf, auth_method: 'password', identifier: 'hordak', password: 'horde_prime_will_rise_unless', @@ -198,7 +168,9 @@ describe('Register with email (magic_link)', () => { .expect(302) .expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf'); - await followMagicLinkFromMail(cookies); + await followMagicLinkFromMail(agent, cookies); + + await testLogout(agent, cookies, csrf); // Verify saved user const user = await User.select() @@ -250,23 +222,23 @@ describe('Register with email (magic_link)', () => { .expect(302) .expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf'); - await followMagicLinkFromMail(cookies); + await followMagicLinkFromMail(agent, cookies); + + await testLogout(agent, cookies, csrf); // Verify saved user const user = await User.select() .with('mainEmail') - .where('name', 'glimmer') + .where('name', 'angella') .first(); expect(user).toBeDefined(); // Attempt register with another mail but same username - const res2 = await agent.get('/csrf').expect(200); - await agent.post('/auth/register') - .set('Cookie', res2.get('Set-Cookie')) + .set('Cookie', cookies) .send({ - csrf: res2.text, + csrf: csrf, auth_method: 'magic_link', identifier: 'angella_something_else@example.org', name: 'angella', @@ -292,23 +264,23 @@ describe('Register with email (magic_link)', () => { .expect(302) .expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf'); - await followMagicLinkFromMail(cookies); + await followMagicLinkFromMail(agent, cookies); + + await testLogout(agent, cookies, csrf); // Verify saved user const user = await User.select() .with('mainEmail') - .where('name', 'glimmer') + .where('name', 'bow') .first(); expect(user).toBeDefined(); // Attempt register with another mail but same username - const res2 = await agent.get('/csrf').expect(200); - await agent.post('/auth/register') - .set('Cookie', res2.get('Set-Cookie')) + .set('Cookie', cookies) .send({ - csrf: res2.text, + csrf: csrf, auth_method: 'magic_link', identifier: 'bow@example.org', name: 'bow2', @@ -352,7 +324,7 @@ describe('Authenticate with username and password (password)', () => { .expect(302) .expect('Location', '/'); - await testLogout(cookies, csrf); + await testLogout(agent, cookies, csrf); }); test('Automatic auth_method', async () => { @@ -385,7 +357,7 @@ describe('Authenticate with username and password (password)', () => { .expect(302) .expect('Location', '/'); - await testLogout(cookies, csrf); + await testLogout(agent, cookies, csrf); }); test('Non-existing username', async () => { @@ -495,9 +467,9 @@ describe('Authenticate with email (magic_link)', () => { .expect(302) .expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf'); - await followMagicLinkFromMail(cookies); + await followMagicLinkFromMail(agent, cookies); - await testLogout(cookies, csrf); + await testLogout(agent, cookies, csrf); }); test('Automatic auth_method', async () => { @@ -518,9 +490,9 @@ describe('Authenticate with email (magic_link)', () => { .expect(302) .expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf'); - await followMagicLinkFromMail(cookies); + await followMagicLinkFromMail(agent, cookies); - await testLogout(cookies, csrf); + await testLogout(agent, cookies, csrf); }); test('Non-existing email (forced auth_method)', async () => { @@ -582,7 +554,9 @@ describe('Authenticate with email and password (password)', () => { .expect(302) .expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf'); - await followMagicLinkFromMail(cookies); + await followMagicLinkFromMail(agent, cookies); + + await testLogout(agent, cookies, csrf); // Verify saved user const user = await User.select() @@ -635,7 +609,7 @@ describe('Authenticate with email and password (password)', () => { .expect(302) .expect('Location', '/'); - await testLogout(cookies, csrf); + await testLogout(agent, cookies, csrf); }); test('Automatic auth_method', async () => { @@ -668,6 +642,6 @@ describe('Authenticate with email and password (password)', () => { .expect(302) .expect('Location', '/'); - await testLogout(cookies, csrf); + await testLogout(agent, cookies, csrf); }); }); diff --git a/test/AuthenticationNoUsername.test.ts b/test/AuthenticationNoUsername.test.ts new file mode 100644 index 0000000..31ef118 --- /dev/null +++ b/test/AuthenticationNoUsername.test.ts @@ -0,0 +1,147 @@ +import TestApp from "../src/TestApp"; +import useApp from "./_app"; +import Controller from "../src/Controller"; +import supertest from "supertest"; +import CsrfProtectionComponent from "../src/components/CsrfProtectionComponent"; +import UserPasswordComponent from "../src/auth/password/UserPasswordComponent"; +import {popEmail} from "./_mail_server"; +import AuthComponent from "../src/auth/AuthComponent"; +import Migration, {MigrationType} from "../src/db/Migration"; +import AddNameToUsersMigration from "../src/auth/migrations/AddNameToUsersMigration"; +import {followMagicLinkFromMail, testLogout} from "./_authentication_common"; +import UserEmail from "../src/auth/models/UserEmail"; + +let app: TestApp; +useApp(async (addr, port) => { + return app = new class extends TestApp { + protected async init(): Promise { + this.use(new class extends Controller { + public routes(): void { + this.get('/', (req, res) => { + res.render('home'); + }, 'home'); + this.get('/csrf', (req, res) => { + res.send(CsrfProtectionComponent.getCsrfToken(req.getSession())); + }, 'csrf'); + this.get('/is-auth', async (req, res) => { + const proofs = await this.getApp().as(AuthComponent).getAuthGuard().getProofs(req); + if (proofs.length > 0) res.sendStatus(200); + else res.sendStatus(401); + }, 'is-auth'); + } + }()); + + await super.init(); + } + + protected getMigrations(): MigrationType[] { + return super.getMigrations().filter(m => m !== AddNameToUsersMigration); + } + }(addr, port); +}); + +let agent: supertest.SuperTest; + +beforeAll(() => { + agent = supertest(app.getExpressApp()); +}); + +describe('Register with username and password (password)', () => { + test('Must be disabled', async () => { + const res = await agent.get('/csrf').expect(200); + const cookies = res.get('Set-Cookie'); + const csrf = res.text; + + // Register user + await agent.post('/auth/register') + .set('Cookie', cookies) + .send({ + csrf: csrf, + auth_method: 'password', + identifier: 'entrapta', + password: 'darla_is_cute', + password_confirmation: 'darla_is_cute', + terms: 'on', + }) + .expect(500); + }); +}); + +describe('Register with email (magic_link)', () => { + test('General case', async () => { + const res = await agent.get('/csrf').expect(200); + const cookies = res.get('Set-Cookie'); + const csrf = res.text; + + await agent.post('/auth/register') + .set('Cookie', cookies) + .send({ + csrf: csrf, + auth_method: 'magic_link', + identifier: 'glimmer@example.org', + }) + .expect(302) + .expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf'); + + await followMagicLinkFromMail(agent, cookies); + + await testLogout(agent, cookies, csrf); + + // Verify saved user + const email = await UserEmail.select() + .with('user') + .where('email', 'glimmer@example.org') + .first(); + const user = email?.user.getOrFail(); + + expect(user).toBeDefined(); + + expect(email).toBeDefined(); + expect(email?.email).toStrictEqual('glimmer@example.org'); + + await expect(user?.as(UserPasswordComponent).verifyPassword('')).resolves.toStrictEqual(false); + }); + + test('Cannot register taken email', async () => { + const res = await agent.get('/csrf').expect(200); + const cookies = res.get('Set-Cookie'); + const csrf = res.text; + + await agent.post('/auth/register') + .set('Cookie', cookies) + .send({ + csrf: csrf, + auth_method: 'magic_link', + identifier: 'bow@example.org', + name: 'bow', + }) + .expect(302) + .expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf'); + + await followMagicLinkFromMail(agent, cookies); + + // Verify saved user + const userEmail = await UserEmail.select() + .with('user') + .where('email', 'bow@example.org') + .first(); + const user = userEmail?.user.getOrFail(); + + expect(user).toBeDefined(); + + // Attempt register with another mail but same username + const res2 = await agent.get('/csrf').expect(200); + + await agent.post('/auth/register') + .set('Cookie', res2.get('Set-Cookie')) + .send({ + csrf: res2.text, + auth_method: 'magic_link', + identifier: 'bow@example.org', + name: 'bow2', + }) + .expect(400); + + expect(await popEmail()).toBeNull(); + }); +}); diff --git a/test/Model.test.ts b/test/Model.test.ts index ff1cf67..2734d80 100644 --- a/test/Model.test.ts +++ b/test/Model.test.ts @@ -5,6 +5,7 @@ import {ValidationBag} from "../src/db/Validator"; import {log} from "../src/Logger"; import {ManyThroughModelRelation, OneModelRelation} from "../src/db/ModelRelation"; import {MIGRATIONS} from "../src/TestApp"; +import config from "config"; class FakeDummyModel extends Model { public id?: number = undefined; @@ -99,6 +100,10 @@ let roleFactory: ModelFactory; let permissionFactory: ModelFactory; beforeAll(async () => { + await MysqlConnectionManager.prepare(); + await MysqlConnectionManager.query('DROP DATABASE IF EXISTS ' + config.get('mysql.database')); + await MysqlConnectionManager.endPool(); + log.setSettings({minLevel: "trace"}); MysqlConnectionManager.registerMigrations(MIGRATIONS); ModelFactory.register(FakeDummyModel); diff --git a/test/_app.ts b/test/_app.ts index bb29469..643d66d 100644 --- a/test/_app.ts +++ b/test/_app.ts @@ -1,12 +1,18 @@ import Application from "../src/Application"; import {setupMailServer, teardownMailServer} from "./_mail_server"; import TestApp from "../src/TestApp"; +import MysqlConnectionManager from "../src/db/MysqlConnectionManager"; +import config from "config"; export default function useApp(appSupplier?: (addr: string, port: number) => Promise): void { let app: Application; beforeAll(async (done) => { + await MysqlConnectionManager.prepare(); + await MysqlConnectionManager.query('DROP DATABASE IF EXISTS ' + config.get('mysql.database')); + await MysqlConnectionManager.endPool(); + await setupMailServer(); app = appSupplier ? await appSupplier('127.0.0.1', 8966) : new TestApp('127.0.0.1', 8966); diff --git a/test/_authentication_common.ts b/test/_authentication_common.ts new file mode 100644 index 0000000..55741ad --- /dev/null +++ b/test/_authentication_common.ts @@ -0,0 +1,38 @@ +import {popEmail} from "./_mail_server"; +import supertest from "supertest"; + +export async function followMagicLinkFromMail( + agent: supertest.SuperTest, + cookies: string[], +): Promise { + const mail: Record | null = await popEmail(); + expect(mail).not.toBeNull(); + + const query = (mail?.text as string).split('/magic/link?')[1].split('\n')[0]; + expect(query).toBeDefined(); + + await agent.get('/magic/link?' + query) + .expect(200); + await agent.get('/magic/lobby') + .set('Cookie', cookies) + .expect(302) + .expect('Location', '/'); +} + +export async function testLogout( + agent: supertest.SuperTest, + cookies: string[], + csrf: string, +): Promise { + // Authenticated + await agent.get('/is-auth').set('Cookie', cookies).expect(200); + + // Logout + await agent.post('/auth/logout') + .set('Cookie', cookies) + .send({csrf: csrf}) + .expect(302); + + // Not authenticated + await agent.get('/is-auth').set('Cookie', cookies).expect(401); +}