mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 02:26:21 +08:00
refactor(server): auth (#7994)
This commit is contained in:
+3
-3
@@ -8,8 +8,8 @@ import type { INestApplication } from '@nestjs/common';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
|
||||
import { AuthService } from '../src/core/auth/service';
|
||||
import { MailService } from '../src/fundamentals/mailer';
|
||||
import { AuthService } from '../../src/core/auth/service';
|
||||
import { MailService } from '../../src/fundamentals/mailer';
|
||||
import {
|
||||
changeEmail,
|
||||
changePassword,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
sendSetPasswordEmail,
|
||||
sendVerifyChangeEmail,
|
||||
signUp,
|
||||
} from './utils';
|
||||
} from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
@@ -20,7 +20,7 @@ const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
test.before(async t => {
|
||||
const { app } = await createTestingApp({
|
||||
imports: [FeatureModule, UserModule, AuthModule],
|
||||
tapModule: m => {
|
||||
@@ -36,10 +36,14 @@ test.beforeEach(async t => {
|
||||
t.context.mailer = app.get(MailService);
|
||||
t.context.app = app;
|
||||
|
||||
t.context.u1 = await t.context.auth.signUp('u1', 'u1@affine.pro', '1');
|
||||
t.context.u1 = await t.context.auth.signUp('u1@affine.pro', '1');
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
test.beforeEach(() => {
|
||||
Sinon.reset();
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { Controller, Get, HttpStatus, INestApplication } from '@nestjs/common';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
import request from 'supertest';
|
||||
|
||||
import {
|
||||
AuthGuard,
|
||||
AuthModule,
|
||||
CurrentUser,
|
||||
Public,
|
||||
} from '../../src/core/auth';
|
||||
import { AuthModule, CurrentUser, Public, Session } from '../../src/core/auth';
|
||||
import { AuthService } from '../../src/core/auth/service';
|
||||
import { createTestingApp } from '../utils';
|
||||
|
||||
@@ -25,115 +20,123 @@ class TestController {
|
||||
private(@CurrentUser() user: CurrentUser) {
|
||||
return { user };
|
||||
}
|
||||
|
||||
@Get('/session')
|
||||
session(@Session() session: Session) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
auth: Sinon.SinonStubbedInstance<AuthService>;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
let server!: any;
|
||||
let auth!: AuthService;
|
||||
let u1!: CurrentUser;
|
||||
|
||||
test.before(async t => {
|
||||
const { app } = await createTestingApp({
|
||||
imports: [AuthModule],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: AuthGuard,
|
||||
},
|
||||
],
|
||||
controllers: [TestController],
|
||||
tapModule: m => {
|
||||
m.overrideProvider(AuthService).useValue(
|
||||
Sinon.createStubInstance(AuthService)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
t.context.auth = app.get(AuthService);
|
||||
auth = app.get(AuthService);
|
||||
u1 = await auth.signUp('u1@affine.pro', '1');
|
||||
|
||||
const db = app.get(PrismaClient);
|
||||
await db.session.create({
|
||||
data: {
|
||||
id: '1',
|
||||
},
|
||||
});
|
||||
await auth.createUserSession(u1.id, '1');
|
||||
|
||||
server = app.getHttpServer();
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should be able to visit public api if not signed in', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const res = await request(app.getHttpServer()).get('/public').expect(200);
|
||||
const res = await request(server).get('/public').expect(200);
|
||||
|
||||
t.is(res.body.user, undefined);
|
||||
});
|
||||
|
||||
test('should be able to visit public api if signed in', async t => {
|
||||
const { app, auth } = t.context;
|
||||
|
||||
// @ts-expect-error mock
|
||||
auth.getUserSession.resolves({ user: { id: '1' }, session: { id: '1' } });
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
const res = await request(server)
|
||||
.get('/public')
|
||||
.set('Cookie', `${AuthService.sessionCookieName}=1`)
|
||||
.expect(HttpStatus.OK);
|
||||
|
||||
t.is(res.body.user.id, '1');
|
||||
t.is(res.body.user.id, u1.id);
|
||||
});
|
||||
|
||||
test('should not be able to visit private api if not signed in', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get('/private')
|
||||
.expect(HttpStatus.UNAUTHORIZED)
|
||||
.expect({
|
||||
status: 401,
|
||||
code: 'Unauthorized',
|
||||
type: 'AUTHENTICATION_REQUIRED',
|
||||
name: 'AUTHENTICATION_REQUIRED',
|
||||
message: 'You must sign in first to access this resource.',
|
||||
});
|
||||
await request(server).get('/private').expect(HttpStatus.UNAUTHORIZED).expect({
|
||||
status: 401,
|
||||
code: 'Unauthorized',
|
||||
type: 'AUTHENTICATION_REQUIRED',
|
||||
name: 'AUTHENTICATION_REQUIRED',
|
||||
message: 'You must sign in first to access this resource.',
|
||||
});
|
||||
|
||||
t.assert(true);
|
||||
});
|
||||
|
||||
test('should be able to visit private api if signed in', async t => {
|
||||
const { app, auth } = t.context;
|
||||
|
||||
// @ts-expect-error mock
|
||||
auth.getUserSession.resolves({ user: { id: '1' }, session: { id: '1' } });
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
const res = await request(server)
|
||||
.get('/private')
|
||||
.set('Cookie', `${AuthService.sessionCookieName}=1`)
|
||||
.expect(HttpStatus.OK);
|
||||
|
||||
t.is(res.body.user.id, '1');
|
||||
t.is(res.body.user.id, u1.id);
|
||||
});
|
||||
|
||||
test('should be able to parse session cookie', async t => {
|
||||
const { app, auth } = t.context;
|
||||
|
||||
// @ts-expect-error mock
|
||||
auth.getUserSession.resolves({ user: { id: '1' }, session: { id: '1' } });
|
||||
|
||||
await request(app.getHttpServer())
|
||||
const spy = Sinon.spy(auth, 'getUserSession');
|
||||
await request(server)
|
||||
.get('/public')
|
||||
.set('cookie', `${AuthService.sessionCookieName}=1`)
|
||||
.expect(200);
|
||||
|
||||
t.deepEqual(auth.getUserSession.firstCall.args, ['1', 0]);
|
||||
t.deepEqual(spy.firstCall.args, ['1', undefined]);
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
test('should be able to parse bearer token', async t => {
|
||||
const { app, auth } = t.context;
|
||||
const spy = Sinon.spy(auth, 'getUserSession');
|
||||
|
||||
// @ts-expect-error mock
|
||||
auth.getUserSession.resolves({ user: { id: '1' }, session: { id: '1' } });
|
||||
|
||||
await request(app.getHttpServer())
|
||||
await request(server)
|
||||
.get('/public')
|
||||
.auth('1', { type: 'bearer' })
|
||||
.expect(200);
|
||||
|
||||
t.deepEqual(auth.getUserSession.firstCall.args, ['1', 0]);
|
||||
t.deepEqual(spy.firstCall.args, ['1', undefined]);
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
test('should be able to refresh session if needed', async t => {
|
||||
await t.context.app.get(PrismaClient).userSession.updateMany({
|
||||
where: {
|
||||
sessionId: '1',
|
||||
},
|
||||
data: {
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 /* expires in 1 hour */),
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request(server)
|
||||
.get('/session')
|
||||
.set('cookie', `${AuthService.sessionCookieName}=1`)
|
||||
.expect(200);
|
||||
|
||||
const cookie = res
|
||||
.get('Set-Cookie')
|
||||
?.find(c => c.startsWith(AuthService.sessionCookieName));
|
||||
|
||||
t.truthy(cookie);
|
||||
});
|
||||
|
||||
@@ -3,11 +3,11 @@ import { PrismaClient } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
|
||||
import { CurrentUser } from '../../src/core/auth';
|
||||
import { AuthService, parseAuthUserSeqNum } from '../../src/core/auth/service';
|
||||
import { AuthService } from '../../src/core/auth/service';
|
||||
import { FeatureModule } from '../../src/core/features';
|
||||
import { QuotaModule } from '../../src/core/quota';
|
||||
import { UserModule, UserService } from '../../src/core/user';
|
||||
import { createTestingModule } from '../utils';
|
||||
import { createTestingModule, initTestingDB } from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
auth: AuthService;
|
||||
@@ -17,7 +17,7 @@ const test = ava as TestFn<{
|
||||
m: TestingModule;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
test.before(async t => {
|
||||
const m = await createTestingModule({
|
||||
imports: [QuotaModule, FeatureModule, UserModule],
|
||||
providers: [AuthService],
|
||||
@@ -27,50 +27,18 @@ test.beforeEach(async t => {
|
||||
t.context.user = m.get(UserService);
|
||||
t.context.db = m.get(PrismaClient);
|
||||
t.context.m = m;
|
||||
|
||||
t.context.u1 = await t.context.auth.signUp('u1', 'u1@affine.pro', '1');
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
test.beforeEach(async t => {
|
||||
await initTestingDB(t.context.db);
|
||||
t.context.u1 = await t.context.auth.signUp('u1@affine.pro', '1');
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.m.close();
|
||||
});
|
||||
|
||||
test('should be able to parse auth user seq num', t => {
|
||||
t.deepEqual(
|
||||
[
|
||||
'1',
|
||||
'2',
|
||||
3,
|
||||
-3,
|
||||
'-4',
|
||||
'1.1',
|
||||
'str',
|
||||
'1111111111111111111111111111111111111111111',
|
||||
].map(parseAuthUserSeqNum),
|
||||
[1, 2, 3, 0, 0, 0, 0, 0]
|
||||
);
|
||||
});
|
||||
|
||||
test('should be able to sign up', async t => {
|
||||
const { auth } = t.context;
|
||||
const u2 = await auth.signUp('u2', 'u2@affine.pro', '1');
|
||||
|
||||
t.is(u2.email, 'u2@affine.pro');
|
||||
|
||||
const signedU2 = await auth.signIn(u2.email, '1');
|
||||
|
||||
t.is(u2.email, signedU2.email);
|
||||
});
|
||||
|
||||
test('should throw if email duplicated', async t => {
|
||||
const { auth } = t.context;
|
||||
|
||||
await t.throwsAsync(() => auth.signUp('u1', 'u1@affine.pro', '1'), {
|
||||
message: 'This email has already been registered.',
|
||||
});
|
||||
});
|
||||
|
||||
test('should be able to sign in', async t => {
|
||||
test('should be able to sign in by password', async t => {
|
||||
const { auth } = t.context;
|
||||
|
||||
const signedInUser = await auth.signIn('u1@affine.pro', '1');
|
||||
@@ -114,7 +82,7 @@ test('should be able to change password', async t => {
|
||||
let signedInU1 = await auth.signIn('u1@affine.pro', '1');
|
||||
t.is(signedInU1.email, u1.email);
|
||||
|
||||
await auth.changePassword(u1.id, '2');
|
||||
await auth.changePassword(u1.id, 'hello world affine');
|
||||
|
||||
await t.throwsAsync(
|
||||
() => auth.signIn('u1@affine.pro', '1' /* old password */),
|
||||
@@ -123,7 +91,7 @@ test('should be able to change password', async t => {
|
||||
}
|
||||
);
|
||||
|
||||
signedInU1 = await auth.signIn('u1@affine.pro', '2');
|
||||
signedInU1 = await auth.signIn('u1@affine.pro', 'hello world affine');
|
||||
t.is(signedInU1.email, u1.email);
|
||||
});
|
||||
|
||||
@@ -147,7 +115,7 @@ test('should be able to change email', async t => {
|
||||
test('should be able to create user session', async t => {
|
||||
const { auth, u1 } = t.context;
|
||||
|
||||
const session = await auth.createUserSession(u1);
|
||||
const session = await auth.createUserSession(u1.id);
|
||||
|
||||
t.is(session.userId, u1.id);
|
||||
});
|
||||
@@ -155,7 +123,7 @@ test('should be able to create user session', async t => {
|
||||
test('should be able to get user from session', async t => {
|
||||
const { auth, u1 } = t.context;
|
||||
|
||||
const session = await auth.createUserSession(u1);
|
||||
const session = await auth.createUserSession(u1.id);
|
||||
|
||||
const userSession = await auth.getUserSession(session.sessionId);
|
||||
|
||||
@@ -166,23 +134,50 @@ test('should be able to get user from session', async t => {
|
||||
test('should be able to sign out session', async t => {
|
||||
const { auth, u1 } = t.context;
|
||||
|
||||
const session = await auth.createUserSession(u1);
|
||||
const session = await auth.createUserSession(u1.id);
|
||||
await auth.signOut(session.sessionId);
|
||||
const userSession = await auth.getUserSession(session.sessionId);
|
||||
|
||||
const signedOutSession = await auth.signOut(session.sessionId);
|
||||
t.is(userSession, null);
|
||||
});
|
||||
|
||||
t.is(signedOutSession, null);
|
||||
test('should not return expired session', async t => {
|
||||
const { auth, u1, db } = t.context;
|
||||
|
||||
const session = await auth.createUserSession(u1.id);
|
||||
|
||||
await db.userSession.update({
|
||||
where: { id: session.id },
|
||||
data: {
|
||||
expiresAt: new Date(Date.now() - 1000),
|
||||
},
|
||||
});
|
||||
|
||||
const userSession = await auth.getUserSession(session.sessionId);
|
||||
t.is(userSession, null);
|
||||
});
|
||||
|
||||
// Tests for Multi-Accounts Session
|
||||
test('should be able to sign in different user in a same session', async t => {
|
||||
const { auth, u1 } = t.context;
|
||||
|
||||
const u2 = await auth.signUp('u2', 'u2@affine.pro', '1');
|
||||
const u2 = await auth.signUp('u2@affine.pro', '1');
|
||||
|
||||
const session = await auth.createUserSession(u1);
|
||||
await auth.createUserSession(u2, session.sessionId);
|
||||
const session = await auth.createSession();
|
||||
|
||||
const [signedU1, signedU2] = await auth.getUserList(session.sessionId);
|
||||
await auth.createUserSession(u1.id, session.id);
|
||||
|
||||
let userList = await auth.getUserList(session.id);
|
||||
t.is(userList.length, 1);
|
||||
t.is(userList[0]!.id, u1.id);
|
||||
|
||||
await auth.createUserSession(u2.id, session.id);
|
||||
|
||||
userList = await auth.getUserList(session.id);
|
||||
|
||||
t.is(userList.length, 2);
|
||||
|
||||
const [signedU1, signedU2] = userList;
|
||||
|
||||
t.not(signedU1, null);
|
||||
t.not(signedU2, null);
|
||||
@@ -193,29 +188,30 @@ test('should be able to sign in different user in a same session', async t => {
|
||||
test('should be able to signout multi accounts session', async t => {
|
||||
const { auth, u1 } = t.context;
|
||||
|
||||
const u2 = await auth.signUp('u2', 'u2@affine.pro', '1');
|
||||
const u2 = await auth.signUp('u2@affine.pro', '1');
|
||||
|
||||
const session = await auth.createUserSession(u1);
|
||||
await auth.createUserSession(u2, session.sessionId);
|
||||
const session = await auth.createSession();
|
||||
|
||||
// sign out user at seq(0)
|
||||
let signedOutSession = await auth.signOut(session.sessionId);
|
||||
await auth.createUserSession(u1.id, session.id);
|
||||
await auth.createUserSession(u2.id, session.id);
|
||||
|
||||
t.not(signedOutSession, null);
|
||||
await auth.signOut(session.id, u1.id);
|
||||
|
||||
const userSession1 = await auth.getUserSession(session.sessionId, 0);
|
||||
const userSession2 = await auth.getUserSession(session.sessionId, 1);
|
||||
let list = await auth.getUserList(session.id);
|
||||
|
||||
t.is(userSession2, null);
|
||||
t.not(userSession1, null);
|
||||
t.is(list.length, 1);
|
||||
t.is(list[0]!.id, u2.id);
|
||||
|
||||
t.is(userSession1!.user.id, u2.id);
|
||||
const u1Session = await auth.getUserSession(session.id, u1.id);
|
||||
|
||||
// sign out user at seq(0)
|
||||
signedOutSession = await auth.signOut(session.sessionId);
|
||||
t.is(u1Session, null);
|
||||
|
||||
t.is(signedOutSession, null);
|
||||
await auth.signOut(session.id, u2.id);
|
||||
list = await auth.getUserList(session.id);
|
||||
|
||||
const userSession3 = await auth.getUserSession(session.sessionId, 0);
|
||||
t.is(userSession3, null);
|
||||
t.is(list.length, 0);
|
||||
|
||||
const u2Session = await auth.getUserSession(session.id, u2.id);
|
||||
|
||||
t.is(u2Session, null);
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ const test = ava as TestFn<{
|
||||
m: TestingModule;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
test.before(async t => {
|
||||
const m = await createTestingModule({
|
||||
providers: [TokenService],
|
||||
});
|
||||
@@ -19,7 +19,7 @@ test.beforeEach(async t => {
|
||||
t.context.m = m;
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
test.after.always(async t => {
|
||||
await t.context.m.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ test.afterEach.always(async t => {
|
||||
let userId: string;
|
||||
test.beforeEach(async t => {
|
||||
const { auth } = t.context;
|
||||
const user = await auth.signUp('test', 'darksky@affine.pro', '123456');
|
||||
const user = await auth.signUp('test@affine.pro', '123456');
|
||||
userId = user.id;
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ test('should be able to fork chat session', async t => {
|
||||
});
|
||||
t.not(sessionId, forkedSessionId1, 'should fork a new session');
|
||||
|
||||
const newUser = await auth.signUp('test', 'darksky.1@affine.pro', '123456');
|
||||
const newUser = await auth.signUp('darksky.1@affine.pro', '123456');
|
||||
const forkedSessionId2 = await session.fork({
|
||||
userId: newUser.id,
|
||||
sessionId,
|
||||
|
||||
@@ -82,7 +82,7 @@ test.afterEach.always(async t => {
|
||||
test('should be able to set user feature', async t => {
|
||||
const { auth, feature } = t.context;
|
||||
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
const u1 = await auth.signUp('test@test.com', '123456');
|
||||
|
||||
const f1 = await feature.getUserFeatures(u1.id);
|
||||
t.is(f1.length, 0, 'should be empty');
|
||||
@@ -96,7 +96,7 @@ test('should be able to set user feature', async t => {
|
||||
|
||||
test('should be able to check early access', async t => {
|
||||
const { auth, feature, management } = t.context;
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
const u1 = await auth.signUp('test@test.com', '123456');
|
||||
|
||||
const f1 = await management.canEarlyAccess(u1.email);
|
||||
t.false(f1, 'should not have early access');
|
||||
@@ -112,7 +112,7 @@ test('should be able to check early access', async t => {
|
||||
|
||||
test('should be able revert user feature', async t => {
|
||||
const { auth, feature, management } = t.context;
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
const u1 = await auth.signUp('test@test.com', '123456');
|
||||
|
||||
const f1 = await management.canEarlyAccess(u1.email);
|
||||
t.false(f1, 'should not have early access');
|
||||
@@ -138,7 +138,7 @@ test('should be able revert user feature', async t => {
|
||||
|
||||
test('should be same instance after reset the user feature', async t => {
|
||||
const { auth, feature, management } = t.context;
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
const u1 = await auth.signUp('test@test.com', '123456');
|
||||
|
||||
await management.addEarlyAccess(u1.id);
|
||||
const f1 = (await feature.getUserFeatures(u1.id))[0];
|
||||
@@ -154,7 +154,7 @@ test('should be same instance after reset the user feature', async t => {
|
||||
test('should be able to set workspace feature', async t => {
|
||||
const { auth, feature, workspace } = t.context;
|
||||
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
const u1 = await auth.signUp('test@test.com', '123456');
|
||||
const w1 = await workspace.createWorkspace(u1, null);
|
||||
|
||||
const f1 = await feature.getWorkspaceFeatures(w1.id);
|
||||
@@ -169,7 +169,7 @@ test('should be able to set workspace feature', async t => {
|
||||
|
||||
test('should be able to check workspace feature', async t => {
|
||||
const { auth, feature, workspace, management } = t.context;
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
const u1 = await auth.signUp('test@test.com', '123456');
|
||||
const w1 = await workspace.createWorkspace(u1, null);
|
||||
|
||||
const f1 = await management.hasWorkspaceFeature(w1.id, FeatureType.Copilot);
|
||||
@@ -186,7 +186,7 @@ test('should be able to check workspace feature', async t => {
|
||||
|
||||
test('should be able revert workspace feature', async t => {
|
||||
const { auth, feature, workspace, management } = t.context;
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
const u1 = await auth.signUp('test@test.com', '123456');
|
||||
const w1 = await workspace.createWorkspace(u1, null);
|
||||
|
||||
const f1 = await management.hasWorkspaceFeature(w1.id, FeatureType.Copilot);
|
||||
|
||||
@@ -33,7 +33,7 @@ test.afterEach.always(async t => {
|
||||
|
||||
test('should include callbackUrl in sending email', async t => {
|
||||
const { auth } = t.context;
|
||||
await auth.signUp('Alex Yang', 'alexyang@example.org', '123456');
|
||||
await auth.signUp('test@affine.pro', '123456');
|
||||
for (const fn of [
|
||||
'sendSetPasswordEmail',
|
||||
'sendChangeEmail',
|
||||
@@ -41,7 +41,7 @@ test('should include callbackUrl in sending email', async t => {
|
||||
'sendVerifyChangeEmail',
|
||||
] as const) {
|
||||
const prev = await getCurrentMailMessageCount();
|
||||
await auth[fn]('alexyang@example.org', 'https://test.com/callback');
|
||||
await auth[fn]('test@affine.pro', 'https://test.com/callback');
|
||||
const current = await getCurrentMailMessageCount();
|
||||
const mail = await getLatestMailMessage();
|
||||
t.regex(
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
INestApplication,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
import request, { type Response } from 'supertest';
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
Throttle,
|
||||
ThrottlerStorage,
|
||||
} from '../../src/fundamentals/throttler';
|
||||
import { createTestingApp, internalSignIn } from '../utils';
|
||||
import { createTestingApp, initTestingDB, internalSignIn } from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
storage: ThrottlerStorage;
|
||||
@@ -93,7 +94,7 @@ class NonThrottledController {
|
||||
}
|
||||
}
|
||||
|
||||
test.beforeEach(async t => {
|
||||
test.before(async t => {
|
||||
const { app } = await createTestingApp({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
@@ -111,14 +112,17 @@ test.beforeEach(async t => {
|
||||
|
||||
t.context.storage = app.get(ThrottlerStorage);
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
test.beforeEach(async t => {
|
||||
await initTestingDB(t.context.app.get(PrismaClient));
|
||||
const { app } = t.context;
|
||||
const auth = app.get(AuthService);
|
||||
const u1 = await auth.signUp('u1', 'u1@affine.pro', 'test');
|
||||
|
||||
const u1 = await auth.signUp('u1@affine.pro', 'test');
|
||||
t.context.cookie = await internalSignIn(app, u1.id);
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ConfigModule } from '../../src/fundamentals/config';
|
||||
import { OAuthProviderName } from '../../src/plugins/oauth/config';
|
||||
import { GoogleOAuthProvider } from '../../src/plugins/oauth/providers/google';
|
||||
import { OAuthService } from '../../src/plugins/oauth/service';
|
||||
import { createTestingApp, getSession } from '../utils';
|
||||
import { createTestingApp, getSession, initTestingDB } from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
auth: AuthService;
|
||||
@@ -26,7 +26,7 @@ const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
test.before(async t => {
|
||||
const { app } = await createTestingApp({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
@@ -50,11 +50,15 @@ test.beforeEach(async t => {
|
||||
t.context.user = app.get(UserService);
|
||||
t.context.db = app.get(PrismaClient);
|
||||
t.context.app = app;
|
||||
|
||||
t.context.u1 = await t.context.auth.signUp('u1', 'u1@affine.pro', '1');
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
test.beforeEach(async t => {
|
||||
Sinon.restore();
|
||||
await initTestingDB(t.context.db);
|
||||
t.context.u1 = await t.context.auth.signUp('u1@affine.pro', '1');
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
@@ -62,10 +66,13 @@ test("should be able to redirect to oauth provider's login page", async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/oauth/login?provider=Google')
|
||||
.expect(HttpStatus.FOUND);
|
||||
.post('/api/oauth/preflight')
|
||||
.send({ provider: 'Google' })
|
||||
.expect(HttpStatus.OK);
|
||||
|
||||
const redirect = new URL(res.header.location);
|
||||
const { url } = res.body;
|
||||
|
||||
const redirect = new URL(url);
|
||||
t.is(redirect.origin, 'https://accounts.google.com');
|
||||
|
||||
t.is(redirect.pathname, '/o/oauth2/v2/auth');
|
||||
@@ -83,7 +90,8 @@ test('should throw if provider is invalid', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get('/oauth/login?provider=Invalid')
|
||||
.post('/api/oauth/preflight')
|
||||
.send({ provider: 'Invalid' })
|
||||
.expect(HttpStatus.BAD_REQUEST)
|
||||
.expect({
|
||||
status: 400,
|
||||
@@ -101,7 +109,6 @@ test('should be able to save oauth state', async t => {
|
||||
const { oauth } = t.context;
|
||||
|
||||
const id = await oauth.saveOAuthState({
|
||||
redirectUri: 'https://example.com',
|
||||
provider: OAuthProviderName.Google,
|
||||
});
|
||||
|
||||
@@ -109,7 +116,6 @@ test('should be able to save oauth state', async t => {
|
||||
|
||||
t.truthy(state);
|
||||
t.is(state!.provider, OAuthProviderName.Google);
|
||||
t.is(state!.redirectUri, 'https://example.com');
|
||||
});
|
||||
|
||||
test('should be able to get registered oauth providers', async t => {
|
||||
@@ -124,7 +130,8 @@ test('should throw if code is missing in callback uri', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get('/oauth/callback')
|
||||
.post('/api/oauth/callback')
|
||||
.send({})
|
||||
.expect(HttpStatus.BAD_REQUEST)
|
||||
.expect({
|
||||
status: 400,
|
||||
@@ -142,7 +149,8 @@ test('should throw if state is missing in callback uri', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get('/oauth/callback?code=1')
|
||||
.post('/api/oauth/callback')
|
||||
.send({ code: '1' })
|
||||
.expect(HttpStatus.BAD_REQUEST)
|
||||
.expect({
|
||||
status: 400,
|
||||
@@ -161,7 +169,8 @@ test('should throw if state is expired', async t => {
|
||||
Sinon.stub(oauth, 'isValidState').resolves(true);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get('/oauth/callback?code=1&state=1')
|
||||
.post('/api/oauth/callback')
|
||||
.send({ code: '1', state: '1' })
|
||||
.expect(HttpStatus.BAD_REQUEST)
|
||||
.expect({
|
||||
status: 400,
|
||||
@@ -178,7 +187,8 @@ test('should throw if state is invalid', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get('/oauth/callback?code=1&state=1')
|
||||
.post('/api/oauth/callback')
|
||||
.send({ code: '1', state: '1' })
|
||||
.expect(HttpStatus.BAD_REQUEST)
|
||||
.expect({
|
||||
status: 400,
|
||||
@@ -199,7 +209,8 @@ test('should throw if provider is missing in state', async t => {
|
||||
Sinon.stub(oauth, 'isValidState').resolves(true);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get(`/oauth/callback?code=1&state=1`)
|
||||
.post('/api/oauth/callback')
|
||||
.send({ code: '1', state: '1' })
|
||||
.expect(HttpStatus.BAD_REQUEST)
|
||||
.expect({
|
||||
status: 400,
|
||||
@@ -221,7 +232,8 @@ test('should throw if provider is invalid in callback uri', async t => {
|
||||
Sinon.stub(oauth, 'isValidState').resolves(true);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get(`/oauth/callback?code=1&state=1`)
|
||||
.post('/api/oauth/callback')
|
||||
.send({ code: '1', state: '1' })
|
||||
.expect(HttpStatus.BAD_REQUEST)
|
||||
.expect({
|
||||
status: 400,
|
||||
@@ -242,7 +254,6 @@ function mockOAuthProvider(app: INestApplication, email: string) {
|
||||
Sinon.stub(oauth, 'isValidState').resolves(true);
|
||||
Sinon.stub(oauth, 'getOAuthState').resolves({
|
||||
provider: OAuthProviderName.Google,
|
||||
redirectUri: '/',
|
||||
});
|
||||
|
||||
// @ts-expect-error mock
|
||||
@@ -260,8 +271,9 @@ test('should be able to sign up with oauth', async t => {
|
||||
mockOAuthProvider(app, 'u2@affine.pro');
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get(`/oauth/callback?code=1&state=1`)
|
||||
.expect(HttpStatus.FOUND);
|
||||
.post(`/api/oauth/callback`)
|
||||
.send({ code: '1', state: '1' })
|
||||
.expect(HttpStatus.OK);
|
||||
|
||||
const session = await getSession(app, res);
|
||||
|
||||
@@ -283,22 +295,17 @@ test('should be able to sign up with oauth', async t => {
|
||||
t.is(user!.connectedAccounts[0].providerAccountId, '1');
|
||||
});
|
||||
|
||||
test('should throw if account register in another way', async t => {
|
||||
test('should not throw if account registered', async t => {
|
||||
const { app, u1 } = t.context;
|
||||
|
||||
mockOAuthProvider(app, u1.email);
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get(`/oauth/callback?code=1&state=1`)
|
||||
.expect(HttpStatus.FOUND);
|
||||
.post(`/api/oauth/callback`)
|
||||
.send({ code: '1', state: '1' })
|
||||
.expect(HttpStatus.OK);
|
||||
|
||||
const link = new URL(res.headers.location);
|
||||
|
||||
t.is(link.pathname, '/signIn');
|
||||
t.is(
|
||||
link.searchParams.get('error'),
|
||||
'You are trying to sign in by a different method than you signed up with.'
|
||||
);
|
||||
t.is(res.body.id, u1.id);
|
||||
});
|
||||
|
||||
test('should be able to fullfil user with oauth sign in', async t => {
|
||||
@@ -313,8 +320,9 @@ test('should be able to fullfil user with oauth sign in', async t => {
|
||||
mockOAuthProvider(app, u3.email);
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get(`/oauth/callback?code=1&state=1`)
|
||||
.expect(HttpStatus.FOUND);
|
||||
.post('/api/oauth/callback')
|
||||
.send({ code: '1', state: '1' })
|
||||
.expect(HttpStatus.OK);
|
||||
|
||||
const session = await getSession(app, res);
|
||||
|
||||
@@ -329,60 +337,3 @@ test('should be able to fullfil user with oauth sign in', async t => {
|
||||
|
||||
t.truthy(account);
|
||||
});
|
||||
|
||||
test('should throw if oauth account already connected', async t => {
|
||||
const { app, db, u1, auth } = t.context;
|
||||
|
||||
await db.connectedAccount.create({
|
||||
data: {
|
||||
userId: u1.id,
|
||||
provider: OAuthProviderName.Google,
|
||||
providerAccountId: '1',
|
||||
},
|
||||
});
|
||||
|
||||
Sinon.stub(auth, 'getUserSession').resolves({
|
||||
user: { id: 'u2-id' },
|
||||
session: {},
|
||||
} as any);
|
||||
|
||||
mockOAuthProvider(app, 'u2@affine.pro');
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get(`/oauth/callback?code=1&state=1`)
|
||||
.set('cookie', `${AuthService.sessionCookieName}=1`)
|
||||
.expect(HttpStatus.FOUND);
|
||||
|
||||
const link = new URL(res.headers.location);
|
||||
|
||||
t.is(link.pathname, '/signIn');
|
||||
t.is(
|
||||
link.searchParams.get('error'),
|
||||
'The third-party account has already been connected to another user.'
|
||||
);
|
||||
});
|
||||
|
||||
test('should be able to connect oauth account', async t => {
|
||||
const { app, u1, auth, db } = t.context;
|
||||
|
||||
Sinon.stub(auth, 'getUserSession').resolves({
|
||||
user: { id: u1.id },
|
||||
session: {},
|
||||
} as any);
|
||||
|
||||
mockOAuthProvider(app, u1.email);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get(`/oauth/callback?code=1&state=1`)
|
||||
.set('cookie', `${AuthService.sessionCookieName}=1`)
|
||||
.expect(HttpStatus.FOUND);
|
||||
|
||||
const account = await db.connectedAccount.findFirst({
|
||||
where: {
|
||||
userId: u1.id,
|
||||
},
|
||||
});
|
||||
|
||||
t.truthy(account);
|
||||
t.is(account!.userId, u1.id);
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ test.beforeEach(async t => {
|
||||
t.context.db = app.get(PrismaClient);
|
||||
t.context.app = app;
|
||||
|
||||
t.context.u1 = await app.get(AuthService).signUp('u1', 'u1@affine.pro', '1');
|
||||
t.context.u1 = await app.get(AuthService).signUp('u1@affine.pro', '1');
|
||||
await t.context.db.userStripeCustomer.create({
|
||||
data: {
|
||||
userId: t.context.u1.id,
|
||||
|
||||
@@ -44,7 +44,7 @@ test.afterEach.always(async t => {
|
||||
test('should be able to set quota', async t => {
|
||||
const { auth, quota } = t.context;
|
||||
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
const u1 = await auth.signUp('test@affine.pro', '123456');
|
||||
|
||||
const q1 = await quota.getUserQuota(u1.id);
|
||||
t.truthy(q1, 'should have quota');
|
||||
@@ -62,7 +62,7 @@ test('should be able to set quota', async t => {
|
||||
|
||||
test('should be able to check storage quota', async t => {
|
||||
const { auth, quota, quotaManager } = t.context;
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
const u1 = await auth.signUp('test@affine.pro', '123456');
|
||||
const freePlan = FreePlan.configs;
|
||||
const proPlan = ProPlan.configs;
|
||||
|
||||
@@ -78,7 +78,7 @@ test('should be able to check storage quota', async t => {
|
||||
|
||||
test('should be able revert quota', async t => {
|
||||
const { auth, quota, quotaManager } = t.context;
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
const u1 = await auth.signUp('test@affine.pro', '123456');
|
||||
const freePlan = FreePlan.configs;
|
||||
const proPlan = ProPlan.configs;
|
||||
|
||||
@@ -113,7 +113,7 @@ test('should be able revert quota', async t => {
|
||||
|
||||
test('should be able to check quota', async t => {
|
||||
const { auth, quotaManager } = t.context;
|
||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||
const u1 = await auth.signUp('test@affine.pro', '123456');
|
||||
const freePlan = FreePlan.configs;
|
||||
|
||||
const q1 = await quotaManager.getUserQuota(u1.id);
|
||||
|
||||
@@ -17,7 +17,7 @@ test.beforeEach(async t => {
|
||||
imports: [AppModule],
|
||||
});
|
||||
|
||||
t.context.u1 = await app.get(AuthService).signUp('u1', 'u1@affine.pro', '1');
|
||||
t.context.u1 = await app.get(AuthService).signUp('u1@affine.pro', '1');
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { gql } from './common';
|
||||
export async function internalSignIn(app: INestApplication, userId: string) {
|
||||
const auth = app.get(AuthService);
|
||||
|
||||
const session = await auth.createUserSession({ id: userId });
|
||||
const session = await auth.createUserSession(userId);
|
||||
|
||||
return `${AuthService.sessionCookieName}=${session.sessionId}`;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export async function signUp(
|
||||
password,
|
||||
emailVerifiedAt: autoVerifyEmail ? new Date() : null,
|
||||
});
|
||||
const { sessionId } = await app.get(AuthService).createUserSession(user);
|
||||
const { sessionId } = await app.get(AuthService).createUserSession(user.id);
|
||||
|
||||
return {
|
||||
...sessionUser(user),
|
||||
|
||||
@@ -33,7 +33,7 @@ test.before(async t => {
|
||||
});
|
||||
|
||||
const auth = app.get(AuthService);
|
||||
t.context.u1 = await auth.signUp('u1', 'u1@affine.pro', '1');
|
||||
t.context.u1 = await auth.signUp('u1@affine.pro', '1');
|
||||
const db = app.get(PrismaClient);
|
||||
|
||||
t.context.db = db;
|
||||
|
||||
Reference in New Issue
Block a user