From 698ace965f4bd32bcb5ef61dafa295ecedf309ff Mon Sep 17 00:00:00 2001
From: Alice Gaudon <alice@gaudon.pro>
Date: Sat, 14 Nov 2020 18:16:58 +0100
Subject: [PATCH] Add authentication tests for username registration

---
 test/Authentication.test.ts          | 123 +++++++++++++++++++++++++++
 test/CsrfProtectionComponent.test.ts |  12 +--
 test/_app.ts                         |   4 +-
 test/_mail_server.ts                 |  21 ++++-
 4 files changed, 148 insertions(+), 12 deletions(-)
 create mode 100644 test/Authentication.test.ts

diff --git a/test/Authentication.test.ts b/test/Authentication.test.ts
new file mode 100644
index 0000000..92a2465
--- /dev/null
+++ b/test/Authentication.test.ts
@@ -0,0 +1,123 @@
+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 MysqlConnectionManager from "../src/db/MysqlConnectionManager";
+import config from "config";
+import {log} from "../src/Logger";
+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";
+
+let app: TestApp;
+useApp(async (addr, port) => {
+    await MysqlConnectionManager.prepare();
+    await MysqlConnectionManager.query('DROP DATABASE IF EXISTS ' + config.get<string>('mysql.database'));
+    await MysqlConnectionManager.endPool();
+
+    return app = new class extends TestApp {
+        protected async init(): Promise<void> {
+            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');
+                }
+            }());
+
+            await super.init();
+        }
+    }(addr, port);
+});
+
+let agent: supertest.SuperTest<supertest.Test>;
+
+describe('Authentication system', () => {
+    test('Obtain session cookies', async () => {
+        agent = supertest(app.getExpressApp());
+    });
+
+    test('Register with email with username', async () => {
+        const res = await agent.get('/csrf').expect(200);
+        const cookies = res.get('Set-Cookie');
+        const csrf = res.text;
+
+        expect(cookies).toBeDefined();
+        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(302)
+            .expect('Location', '/');
+
+
+        // Verify saved user
+        const user = await User.select()
+            .where('name', 'entrapta')
+            .first();
+
+        expect(user).toBeDefined();
+        expect(user?.as(UserNameComponent).name).toStrictEqual('entrapta');
+        await expect(user?.as(UserPasswordComponent).verifyPassword('darla_is_cute')).resolves.toStrictEqual(true);
+    });
+
+    test('Register with email with email (magic_link)', async () => {
+        let res = await agent.get('/csrf').expect(200);
+        const cookies = res.get('Set-Cookie');
+        const csrf = res.text;
+
+        expect(cookies).toBeDefined();
+        res = await agent.post('/auth/register')
+            .set('Cookie', cookies)
+            .send({
+                csrf: csrf,
+                auth_method: 'magic_link',
+                identifier: 'glimmer@example.org',
+                name: 'glimmer',
+            })
+            .expect(302)
+            .expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf');
+
+        const mail: Record<string, unknown> | null = await popEmail();
+        expect(mail).not.toBeNull();
+
+        const query = (mail?.text as string).split('/magic/link?')[1].split('\n')[0];
+        expect(query).toBeDefined();
+
+        // .expect('Location', '/');
+        res = await agent.get('/magic/link?' + query)
+            .expect(200);
+        res = await agent.get('/magic/lobby')
+            .set('Cookie', cookies)
+            .expect(302)
+            .expect('Location', '/');
+        log.debug(res.status, res.headers, res.body, res.text);
+
+
+        // Verify saved user
+        const user = await User.select()
+            .with('mainEmail')
+            .where('name', 'glimmer')
+            .first();
+
+        expect(user).toBeDefined();
+
+        const email = user?.mainEmail.getOrFail();
+        expect(email).toBeDefined();
+        expect(email?.email).toStrictEqual('glimmer@example.org');
+
+        expect(user?.as(UserNameComponent).name).toStrictEqual('glimmer');
+        await expect(user?.as(UserPasswordComponent).verifyPassword('')).resolves.toStrictEqual(false);
+    });
+});
diff --git a/test/CsrfProtectionComponent.test.ts b/test/CsrfProtectionComponent.test.ts
index 33999f2..c418f6e 100644
--- a/test/CsrfProtectionComponent.test.ts
+++ b/test/CsrfProtectionComponent.test.ts
@@ -1,10 +1,10 @@
-import useApp, {TestApp} from "./_app";
+import useApp from "./_app";
 import Controller from "../src/Controller";
-import CsrfProtectionComponent from "../src/components/CsrfProtectionComponent";
 import supertest from "supertest";
+import TestApp from "../src/TestApp";
 
 let app: TestApp;
-useApp((addr, port) => {
+useApp(async (addr, port) => {
     return app = new class extends TestApp {
         protected async init(): Promise<void> {
             this.use(new class extends Controller {
@@ -23,12 +23,6 @@ useApp((addr, port) => {
 
             await super.init();
         }
-
-
-        protected registerComponents() {
-            super.registerComponents();
-            this.use(new CsrfProtectionComponent());
-        }
     }(addr, port);
 });
 
diff --git a/test/_app.ts b/test/_app.ts
index 51b9d38..bb29469 100644
--- a/test/_app.ts
+++ b/test/_app.ts
@@ -3,12 +3,12 @@ import {setupMailServer, teardownMailServer} from "./_mail_server";
 import TestApp from "../src/TestApp";
 
 
-export default function useApp(appSupplier?: (addr: string, port: number) => TestApp): void {
+export default function useApp(appSupplier?: (addr: string, port: number) => Promise<TestApp>): void {
     let app: Application;
 
     beforeAll(async (done) => {
         await setupMailServer();
-        app = appSupplier ? appSupplier('127.0.0.1', 8966) : new TestApp('127.0.0.1', 8966);
+        app = appSupplier ? await appSupplier('127.0.0.1', 8966) : new TestApp('127.0.0.1', 8966);
 
         await app.start();
         done();
diff --git a/test/_mail_server.ts b/test/_mail_server.ts
index 9c914f6..b3aed48 100644
--- a/test/_mail_server.ts
+++ b/test/_mail_server.ts
@@ -1,4 +1,4 @@
-import MailDev from "maildev";
+import MailDev, {Mail} from "maildev";
 
 export const MAIL_SERVER = new MailDev({
     ip: 'localhost',
@@ -17,3 +17,22 @@ export async function teardownMailServer(): Promise<void> {
         else resolve();
     }));
 }
+
+export async function popEmail(): Promise<Record<string, unknown> | null> {
+    return await new Promise<Record<string, unknown> | null>((resolve, reject) => {
+        MAIL_SERVER.getAllEmail((err: Error | undefined, emails: Mail[]) => {
+            if (err) return reject(err);
+            if (emails.length === 0) return resolve(null);
+            const email = emails[0];
+
+            expect(email).toBeDefined();
+            expect(email.id).toBeDefined();
+            return resolve(new Promise<Record<string, unknown>>((resolve, reject) => {
+                MAIL_SERVER.deleteEmail(email.id as string, (err: Error | undefined) => {
+                    if (err) return reject(err);
+                    resolve(email as Record<string, unknown>);
+                });
+            }));
+        });
+    });
+}