Files
AFFiNE/packages/backend/server/src/__tests__/auth/controller.spec.ts
2025-03-19 17:00:19 +00:00

330 lines
8.4 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import { HttpStatus } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import ava, { TestFn } from 'ava';
import Sinon from 'sinon';
import { AuthModule } from '../../core/auth';
import { AuthService } from '../../core/auth/service';
import { FeatureModule } from '../../core/features';
import { UserModule } from '../../core/user';
import {
createTestingApp,
currentUser,
parseCookies,
TestingApp,
} from '../utils';
const test = ava as TestFn<{
auth: AuthService;
db: PrismaClient;
app: TestingApp;
}>;
test.before(async t => {
const app = await createTestingApp({
imports: [FeatureModule, UserModule, AuthModule],
});
t.context.auth = app.get(AuthService);
t.context.db = app.get(PrismaClient);
t.context.app = app;
});
test.beforeEach(async t => {
Sinon.reset();
await t.context.app.initTestingDB();
});
test.after.always(async t => {
await t.context.app.close();
});
test('should be able to sign in with credential', async t => {
const { app } = t.context;
const u1 = await app.createUser('u1@affine.pro');
await app
.POST('/api/auth/sign-in')
.send({ email: u1.email, password: u1.password })
.expect(200);
const session = await currentUser(app);
t.is(session?.id, u1.id);
});
test('should be able to sign in with email', async t => {
const { app } = t.context;
const u1 = await app.createUser('u1@affine.pro');
const res = await app
.POST('/api/auth/sign-in')
.send({ email: u1.email })
.expect(200);
t.is(res.body.email, u1.email);
const signInMail = app.mails.last('SignIn');
t.is(signInMail.to, u1.email);
const url = new URL(signInMail.props.url);
const email = url.searchParams.get('email');
const token = url.searchParams.get('token');
await app.POST('/api/auth/magic-link').send({ email, token }).expect(201);
const session = await currentUser(app);
t.is(session?.id, u1.id);
});
test('should be able to sign up with email', async t => {
const { app } = t.context;
const res = await app
.POST('/api/auth/sign-in')
.send({ email: 'u2@affine.pro' })
.expect(200);
t.is(res.body.email, 'u2@affine.pro');
const signUpMail = app.mails.last('SignUp');
t.is(signUpMail.to, 'u2@affine.pro');
const url = new URL(signUpMail.props.url);
const email = url.searchParams.get('email');
const token = url.searchParams.get('token');
await app.POST('/api/auth/magic-link').send({ email, token }).expect(201);
const session = await currentUser(app);
t.is(session?.email, 'u2@affine.pro');
});
test('should not be able to sign in if email is invalid', async t => {
const { app } = t.context;
const res = await app
.POST('/api/auth/sign-in')
.send({ email: '' })
.expect(400);
t.is(res.body.message, 'An invalid email provided: ');
});
test('should not be able to sign in if forbidden', async t => {
const { app, auth } = t.context;
const u1 = await app.createUser('u1@affine.pro');
const canSignInStub = Sinon.stub(auth, 'canSignIn').resolves(false);
await app
.POST('/api/auth/sign-in')
.send({ email: u1.email })
.expect(HttpStatus.FORBIDDEN);
canSignInStub.restore();
t.pass();
});
test('should be able to sign out', async t => {
const { app } = t.context;
const u1 = await app.createUser('u1@affine.pro');
await app
.POST('/api/auth/sign-in')
.send({ email: u1.email, password: u1.password })
.expect(200);
await app.GET('/api/auth/sign-out').expect(200);
const session = await currentUser(app);
t.falsy(session);
});
test('should be able to correct user id cookie', async t => {
const { app } = t.context;
const u1 = await app.signupV1('u1@affine.pro');
const req = app.GET('/api/auth/session');
let cookies = req.get('cookie') as unknown as string[];
cookies = cookies.filter(c => !c.startsWith(AuthService.userCookieName));
cookies.push(`${AuthService.userCookieName}=invalid_user_id`);
const res = await req.set('Cookie', cookies).expect(200);
const setCookies = parseCookies(res);
const userIdCookie = setCookies[AuthService.userCookieName];
t.is(userIdCookie, u1.id);
});
// multiple accounts session tests
test('should be able to sign in another account in one session', async t => {
const { app } = t.context;
const u1 = await app.createUser('u1@affine.pro');
const u2 = await app.createUser('u2@affine.pro');
// sign in u1
const res = await app
.POST('/api/auth/sign-in')
.send({ email: u1.email, password: u1.password })
.expect(200);
const cookies = parseCookies(res);
// sign in u2 in the same session
await app
.POST('/api/auth/sign-in')
.send({ email: u2.email, password: u2.password })
.expect(200);
// list [u1, u2]
const sessions = await app.GET('/api/auth/sessions').expect(200);
t.is(sessions.body.users.length, 2);
t.like(
sessions.body.users.map((u: any) => u.id),
[u1.id, u2.id]
);
// default to latest signed in user: u2
let session = await app.GET('/api/auth/session').expect(200);
t.is(session.body.user.id, u2.id);
// switch to u1
session = await app
.GET('/api/auth/session')
.set(
'Cookie',
Object.entries(cookies)
.map(([k, v]) => `${k}=${v}`)
.join('; ')
)
.expect(200);
t.is(session.body.user.id, u1.id);
});
test('should be able to sign out multiple accounts in one session', async t => {
const { app } = t.context;
const u1 = await app.signupV1('u1@affine.pro');
const u2 = await app.signupV1('u2@affine.pro');
// sign out u2
await app.GET(`/api/auth/sign-out?user_id=${u2.id}`).expect(200);
// list [u1]
let session = await app.GET('/api/auth/session').expect(200);
t.is(session.body.user.id, u1.id);
// sign in u2 in the same session
await app
.POST('/api/auth/sign-in')
.send({ email: u2.email, password: u2.password })
.expect(200);
// sign out all account in session
await app.GET('/api/auth/sign-out').expect(200);
session = await app.GET('/api/auth/session').expect(200);
t.falsy(session.body.user);
});
test('should be able to sign in with email and client nonce', async t => {
const { app } = t.context;
const clientNonce = randomUUID();
const u1 = await app.createUser();
const res = await app
.POST('/api/auth/sign-in')
.send({ email: u1.email, client_nonce: clientNonce })
.expect(200);
t.is(res.body.email, u1.email);
const signInMail = app.mails.last('SignIn');
t.is(signInMail.to, u1.email);
const url = new URL(signInMail.props.url);
const email = url.searchParams.get('email');
const token = url.searchParams.get('token');
await app
.POST('/api/auth/magic-link')
.send({ email, token, client_nonce: clientNonce })
.expect(201);
const session = await currentUser(app);
t.is(session?.id, u1.id);
});
test('should not be able to sign in with email and client nonce if invalid', async t => {
const { app } = t.context;
const clientNonce = randomUUID();
const u1 = await app.createUser();
const res = await app
.POST('/api/auth/sign-in')
.send({ email: u1.email, client_nonce: clientNonce })
.expect(200);
t.is(res.body.email, u1.email);
const signInMail = app.mails.last('SignIn');
t.is(signInMail.to, u1.email);
const url = new URL(signInMail.props.url);
const email = url.searchParams.get('email');
const token = url.searchParams.get('token');
// invalid client nonce
await app
.POST('/api/auth/magic-link')
.send({ email, token, client_nonce: randomUUID() })
.expect(400)
.expect({
status: 400,
code: 'Bad Request',
type: 'BAD_REQUEST',
name: 'INVALID_AUTH_STATE',
message:
'Invalid auth state. You might start the auth progress from another device.',
});
// no client nonce
await app
.POST('/api/auth/magic-link')
.send({ email, token })
.expect(400)
.expect({
status: 400,
code: 'Bad Request',
type: 'BAD_REQUEST',
name: 'INVALID_AUTH_STATE',
message:
'Invalid auth state. You might start the auth progress from another device.',
});
const session = await currentUser(app);
t.falsy(session);
});
test('should not be able to sign in if token is invalid', async t => {
const { app } = t.context;
const res = await app
.POST('/api/auth/magic-link')
.send({ email: 'u1@affine.pro', token: 'invalid' })
.expect(400);
t.is(res.body.message, 'An invalid email token provided.');
});