test(server): utils (#10028)

This commit is contained in:
forehalo
2025-02-10 06:35:11 +00:00
parent 8f6b512cfd
commit c3f5b4abb4
40 changed files with 1577 additions and 2222 deletions

View File

@@ -20,7 +20,7 @@ test.before('start app', async t => {
renderer: false, renderer: false,
doc: true, doc: true,
} satisfies typeof AFFiNE.flavor; } satisfies typeof AFFiNE.flavor;
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [buildAppModule()], imports: [buildAppModule()],
}); });

View File

@@ -22,7 +22,7 @@ test.before('start app', async t => {
renderer: false, renderer: false,
doc: false, doc: false,
} satisfies typeof AFFiNE.flavor; } satisfies typeof AFFiNE.flavor;
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [buildAppModule()], imports: [buildAppModule()],
}); });

View File

@@ -20,7 +20,7 @@ test.before('start app', async t => {
renderer: true, renderer: true,
doc: false, doc: false,
} satisfies typeof AFFiNE.flavor; } satisfies typeof AFFiNE.flavor;
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [buildAppModule()], imports: [buildAppModule()],
}); });

View File

@@ -40,7 +40,7 @@ test.before('init selfhost server', async t => {
// @ts-expect-error override // @ts-expect-error override
AFFiNE.isSelfhosted = true; AFFiNE.isSelfhosted = true;
AFFiNE.flavor.renderer = true; AFFiNE.flavor.renderer = true;
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [buildAppModule()], imports: [buildAppModule()],
}); });

View File

@@ -20,7 +20,7 @@ test.before('start app', async t => {
renderer: false, renderer: false,
doc: false, doc: false,
} satisfies typeof AFFiNE.flavor; } satisfies typeof AFFiNE.flavor;
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [buildAppModule()], imports: [buildAppModule()],
}); });

View File

@@ -4,12 +4,10 @@ import {
getCurrentMailMessageCount, getCurrentMailMessageCount,
getTokenFromLatestMailMessage, getTokenFromLatestMailMessage,
} from '@affine-test/kit/utils/cloud'; } from '@affine-test/kit/utils/cloud';
import type { INestApplication } from '@nestjs/common';
import type { TestFn } from 'ava'; import type { TestFn } from 'ava';
import ava from 'ava'; import ava from 'ava';
import { MailService } from '../../base/mailer'; import { MailService } from '../../base/mailer';
import { AuthService } from '../../core/auth/service';
import { import {
changeEmail, changeEmail,
changePassword, changePassword,
@@ -18,21 +16,18 @@ import {
sendChangeEmail, sendChangeEmail,
sendSetPasswordEmail, sendSetPasswordEmail,
sendVerifyChangeEmail, sendVerifyChangeEmail,
signUp, TestingApp,
} from '../utils'; } from '../utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
app: INestApplication; app: TestingApp;
auth: AuthService;
mail: MailService; mail: MailService;
}>; }>;
test.beforeEach(async t => { test.beforeEach(async t => {
const { app } = await createTestingApp(); const app = await createTestingApp();
const auth = app.get(AuthService);
const mail = app.get(MailService); const mail = app.get(MailService);
t.context.app = app; t.context.app = app;
t.context.auth = auth;
t.context.mail = mail; t.context.mail = mail;
}); });
@@ -46,11 +41,9 @@ test('change email', async t => {
const u1Email = 'u1@affine.pro'; const u1Email = 'u1@affine.pro';
const u2Email = 'u2@affine.pro'; const u2Email = 'u2@affine.pro';
const u1 = await signUp(app, 'u1', u1Email, '1'); await app.signup(u1Email);
const primitiveMailCount = await getCurrentMailMessageCount(); const primitiveMailCount = await getCurrentMailMessageCount();
await sendChangeEmail(app, u1Email, 'affine.pro');
await sendChangeEmail(app, u1.token.token, u1Email, 'affine.pro');
const afterSendChangeMailCount = await getCurrentMailMessageCount(); const afterSendChangeMailCount = await getCurrentMailMessageCount();
t.is( t.is(
@@ -69,7 +62,6 @@ test('change email', async t => {
await sendVerifyChangeEmail( await sendVerifyChangeEmail(
app, app,
u1.token.token,
changeEmailToken as string, changeEmailToken as string,
u2Email, u2Email,
'affine.pro' 'affine.pro'
@@ -91,7 +83,7 @@ test('change email', async t => {
'fail to get verify change email token from email content' 'fail to get verify change email token from email content'
); );
await changeEmail(app, u1.token.token, verifyEmailToken as string, u2Email); await changeEmail(app, verifyEmailToken as string, u2Email);
const afterNotificationMailCount = await getCurrentMailMessageCount(); const afterNotificationMailCount = await getCurrentMailMessageCount();
@@ -105,15 +97,15 @@ test('change email', async t => {
}); });
test('set and change password', async t => { test('set and change password', async t => {
const { mail, app, auth } = t.context; const { mail, app } = t.context;
if (mail.hasConfigured()) { if (mail.hasConfigured()) {
const u1Email = 'u1@affine.pro'; const u1Email = 'u1@affine.pro';
const u1 = await signUp(app, 'u1', u1Email, '1'); const u1 = await app.signup(u1Email);
const primitiveMailCount = await getCurrentMailMessageCount(); const primitiveMailCount = await getCurrentMailMessageCount();
await sendSetPasswordEmail(app, u1.token.token, u1Email, 'affine.pro'); await sendSetPasswordEmail(app, u1Email, 'affine.pro');
const afterSendSetMailCount = await getCurrentMailMessageCount(); const afterSendSetMailCount = await getCurrentMailMessageCount();
@@ -141,78 +133,84 @@ test('set and change password', async t => {
t.true(success, 'failed to change password'); t.true(success, 'failed to change password');
const ret = auth.signIn(u1Email, newPassword); await app.login({
t.notThrowsAsync(ret, 'failed to check password'); ...u1,
t.is((await ret).id, u1.id, 'failed to check password'); password: newPassword,
});
const user = await currentUser(app);
t.not(user, null, 'failed to get current user');
t.is(user?.email, u1Email, 'failed to get current user');
} }
t.pass(); t.pass();
}); });
test('should revoke token after change user identify', async t => { test('should revoke token after change user identify', async t => {
const { mail, app, auth } = t.context; const { mail, app } = t.context;
if (mail.hasConfigured()) { if (mail.hasConfigured()) {
// change email // change email
{ {
const u1Email = 'u1@affine.pro'; const u1Email = 'u1@affine.pro';
const u2Email = 'u2@affine.pro'; const u2Email = 'u2@affine.pro';
const u1 = await signUp(app, 'u1', u1Email, '1'); const u1 = await app.signup(u1Email);
{ {
const user = await currentUser(app, u1.token.token); const user = await currentUser(app);
t.is(user?.email, u1Email, 'failed to get current user'); t.is(user?.email, u1Email, 'failed to get current user');
} }
await sendChangeEmail(app, u1.token.token, u1Email, 'affine.pro'); await sendChangeEmail(app, u1Email, 'affine.pro');
const changeEmailToken = await getTokenFromLatestMailMessage(); const changeEmailToken = await getTokenFromLatestMailMessage();
await sendVerifyChangeEmail( await sendVerifyChangeEmail(
app, app,
u1.token.token,
changeEmailToken as string, changeEmailToken as string,
u2Email, u2Email,
'affine.pro' 'affine.pro'
); );
const verifyEmailToken = await getTokenFromLatestMailMessage(); const verifyEmailToken = await getTokenFromLatestMailMessage();
await changeEmail( await changeEmail(app, verifyEmailToken as string, u2Email);
app,
u1.token.token,
verifyEmailToken as string,
u2Email
);
const user = await currentUser(app, u1.token.token); let user = await currentUser(app);
t.is(user, null, 'token should be revoked'); t.is(user, null, 'token should be revoked');
const newUserSession = await auth.signIn(u2Email, '1'); await app.login({
t.is(newUserSession?.email, u2Email, 'failed to sign in with new email'); ...u1,
email: u2Email,
});
user = await currentUser(app);
t.is(user?.email, u2Email, 'failed to sign in with new email');
} }
// change password // change password
{ {
const u3Email = 'u3@affine.pro'; const u3Email = 'u3@affine.pro';
const u3 = await signUp(app, 'u1', u3Email, '1'); await app.logout();
const u3 = await app.signup(u3Email);
{ {
const user = await currentUser(app, u3.token.token); const user = await currentUser(app);
t.is(user?.email, u3Email, 'failed to get current user'); t.is(user?.email, u3Email, 'failed to get current user');
} }
await sendSetPasswordEmail(app, u3.token.token, u3Email, 'affine.pro'); await sendSetPasswordEmail(app, u3Email, 'affine.pro');
const token = await getTokenFromLatestMailMessage(); const token = await getTokenFromLatestMailMessage();
const newPassword = randomBytes(16).toString('hex'); const newPassword = randomBytes(16).toString('hex');
await changePassword(app, u3.id, token as string, newPassword); await changePassword(app, u3.id, token as string, newPassword);
const user = await currentUser(app, u3.token.token); let user = await currentUser(app);
t.is(user, null, 'token should be revoked'); t.is(user, null, 'token should be revoked');
const newUserSession = await auth.signIn(u3Email, newPassword); await app.login({
t.is( ...u3,
newUserSession?.email, password: newPassword,
u3Email, });
'failed to sign in with new password' user = await currentUser(app);
); t.is(user?.email, u3Email, 'failed to sign in with new password');
} }
} }
t.pass(); t.pass();

View File

@@ -1,26 +1,29 @@
import { HttpStatus, INestApplication } from '@nestjs/common'; import { HttpStatus } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import ava, { TestFn } from 'ava'; import ava, { TestFn } from 'ava';
import Sinon from 'sinon'; import Sinon from 'sinon';
import request from 'supertest';
import { MailService } from '../../base'; import { MailService } from '../../base';
import { AuthModule, CurrentUser } from '../../core/auth'; import { AuthModule } from '../../core/auth';
import { AuthService } from '../../core/auth/service'; import { AuthService } from '../../core/auth/service';
import { FeatureModule } from '../../core/features'; import { FeatureModule } from '../../core/features';
import { UserModule } from '../../core/user'; import { UserModule } from '../../core/user';
import { createTestingApp, getSession, sessionCookie } from '../utils'; import {
createTestingApp,
currentUser,
parseCookies,
TestingApp,
} from '../utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
auth: AuthService; auth: AuthService;
u1: CurrentUser;
db: PrismaClient; db: PrismaClient;
mailer: Sinon.SinonStubbedInstance<MailService>; mailer: Sinon.SinonStubbedInstance<MailService>;
app: INestApplication; app: TestingApp;
}>; }>;
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [FeatureModule, UserModule, AuthModule], imports: [FeatureModule, UserModule, AuthModule],
tapModule: m => { tapModule: m => {
m.overrideProvider(MailService).useValue( m.overrideProvider(MailService).useValue(
@@ -36,12 +39,11 @@ test.before(async t => {
t.context.db = app.get(PrismaClient); t.context.db = app.get(PrismaClient);
t.context.mailer = app.get(MailService); t.context.mailer = app.get(MailService);
t.context.app = app; t.context.app = app;
t.context.u1 = await t.context.auth.signUp('u1@affine.pro', '1');
}); });
test.beforeEach(() => { test.beforeEach(async t => {
Sinon.reset(); Sinon.reset();
await t.context.app.initTestingDB();
}); });
test.after.always(async t => { test.after.always(async t => {
@@ -49,25 +51,28 @@ test.after.always(async t => {
}); });
test('should be able to sign in with credential', async t => { test('should be able to sign in with credential', async t => {
const { app, u1 } = t.context; const { app } = t.context;
const res = await request(app.getHttpServer()) const u1 = await app.createUser('u1@affine.pro');
.post('/api/auth/sign-in')
.send({ email: u1.email, password: '1' }) await app
.POST('/api/auth/sign-in')
.send({ email: u1.email, password: u1.password })
.expect(200); .expect(200);
const session = await getSession(app, res); const session = await currentUser(app);
t.is(session.user!.id, u1.id); t.is(session?.id, u1.id);
}); });
test('should be able to sign in with email', async t => { test('should be able to sign in with email', async t => {
const { app, u1, mailer } = t.context; const { app, mailer } = t.context;
const u1 = await app.createUser('u1@affine.pro');
// @ts-expect-error mock // @ts-expect-error mock
mailer.sendSignInMail.resolves({ rejected: [] }); mailer.sendSignInMail.resolves({ rejected: [] });
const res = await request(app.getHttpServer()) const res = await app
.post('/api/auth/sign-in') .POST('/api/auth/sign-in')
.send({ email: u1.email }) .send({ email: u1.email })
.expect(200); .expect(200);
@@ -79,13 +84,10 @@ test('should be able to sign in with email', async t => {
const email = url.searchParams.get('email'); const email = url.searchParams.get('email');
const token = url.searchParams.get('token'); const token = url.searchParams.get('token');
const signInRes = await request(app.getHttpServer()) await app.POST('/api/auth/magic-link').send({ email, token }).expect(201);
.post('/api/auth/magic-link')
.send({ email, token })
.expect(201);
const session = await getSession(app, signInRes); const session = await currentUser(app);
t.is(session.user!.id, u1.id); t.is(session?.id, u1.id);
}); });
test('should be able to sign up with email', async t => { test('should be able to sign up with email', async t => {
@@ -94,8 +96,8 @@ test('should be able to sign up with email', async t => {
// @ts-expect-error mock // @ts-expect-error mock
mailer.sendSignUpMail.resolves({ rejected: [] }); mailer.sendSignUpMail.resolves({ rejected: [] });
const res = await request(app.getHttpServer()) const res = await app
.post('/api/auth/sign-in') .POST('/api/auth/sign-in')
.send({ email: 'u2@affine.pro' }) .send({ email: 'u2@affine.pro' })
.expect(200); .expect(200);
@@ -107,20 +109,17 @@ test('should be able to sign up with email', async t => {
const email = url.searchParams.get('email'); const email = url.searchParams.get('email');
const token = url.searchParams.get('token'); const token = url.searchParams.get('token');
const signInRes = await request(app.getHttpServer()) await app.POST('/api/auth/magic-link').send({ email, token }).expect(201);
.post('/api/auth/magic-link')
.send({ email, token })
.expect(201);
const session = await getSession(app, signInRes); const session = await currentUser(app);
t.is(session.user!.email, 'u2@affine.pro'); t.is(session?.email, 'u2@affine.pro');
}); });
test('should not be able to sign in if email is invalid', async t => { test('should not be able to sign in if email is invalid', async t => {
const { app } = t.context; const { app } = t.context;
const res = await request(app.getHttpServer()) const res = await app
.post('/api/auth/sign-in') .POST('/api/auth/sign-in')
.send({ email: '' }) .send({ email: '' })
.expect(400); .expect(400);
@@ -128,12 +127,13 @@ test('should not be able to sign in if email is invalid', async t => {
}); });
test('should not be able to sign in if forbidden', async t => { test('should not be able to sign in if forbidden', async t => {
const { app, auth, u1, mailer } = t.context; const { app, auth, mailer } = t.context;
const u1 = await app.createUser('u1@affine.pro');
const canSignInStub = Sinon.stub(auth, 'canSignIn').resolves(false); const canSignInStub = Sinon.stub(auth, 'canSignIn').resolves(false);
await request(app.getHttpServer()) await app
.post('/api/auth/sign-in') .POST('/api/auth/sign-in')
.send({ email: u1.email }) .send({ email: u1.email })
.expect(HttpStatus.FORBIDDEN); .expect(HttpStatus.FORBIDDEN);
@@ -143,174 +143,109 @@ test('should not be able to sign in if forbidden', async t => {
}); });
test('should be able to sign out', async t => { test('should be able to sign out', async t => {
const { app, u1 } = t.context; const { app } = t.context;
const signInRes = await request(app.getHttpServer()) const u1 = await app.createUser('u1@affine.pro');
.post('/api/auth/sign-in')
.send({ email: u1.email, password: '1' }) await app
.POST('/api/auth/sign-in')
.send({ email: u1.email, password: u1.password })
.expect(200); .expect(200);
const cookie = sessionCookie(signInRes.headers); await app.GET('/api/auth/sign-out').expect(200);
await request(app.getHttpServer()) const session = await currentUser(app);
.get('/api/auth/sign-out')
.set('cookie', cookie)
.expect(200);
const session = await getSession(app, signInRes); t.falsy(session);
t.falsy(session.user);
}); });
test('should be able to correct user id cookie', async t => { test('should be able to correct user id cookie', async t => {
const { app, u1 } = t.context; const { app } = t.context;
const signInRes = await request(app.getHttpServer()) const u1 = await app.signup('u1@affine.pro');
.post('/api/auth/sign-in')
.send({ email: u1.email, password: '1' })
.expect(200);
const cookie = sessionCookie(signInRes.headers); 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];
let session = await request(app.getHttpServer()) t.is(userIdCookie, u1.id);
.get('/api/auth/session')
.set('cookie', cookie)
.expect(200);
let userIdCookie = session.get('Set-Cookie')?.find(c => {
return c.startsWith(`${AuthService.userCookieName}=`);
});
t.true(userIdCookie?.startsWith(`${AuthService.userCookieName}=${u1.id}`));
session = await request(app.getHttpServer())
.get('/api/auth/session')
.set('cookie', `${cookie};${AuthService.userCookieName}=invalid_user_id`)
.expect(200);
userIdCookie = session.get('Set-Cookie')?.find(c => {
return c.startsWith(`${AuthService.userCookieName}=`);
});
t.true(userIdCookie?.startsWith(`${AuthService.userCookieName}=${u1.id}`));
t.is(session.body.user.id, u1.id);
}); });
// multiple accounts session tests // multiple accounts session tests
test('should be able to sign in another account in one session', async t => { test('should be able to sign in another account in one session', async t => {
const { app, u1, auth } = t.context; const { app } = t.context;
const u2 = await auth.signUp('u3@affine.pro', '3'); const u1 = await app.createUser('u1@affine.pro');
const u2 = await app.createUser('u2@affine.pro');
// sign in u1 // sign in u1
const signInRes = await request(app.getHttpServer()) const res = await app
.post('/api/auth/sign-in') .POST('/api/auth/sign-in')
.send({ email: u1.email, password: '1' }) .send({ email: u1.email, password: u1.password })
.expect(200); .expect(200);
const cookie = sessionCookie(signInRes.headers); const cookies = parseCookies(res);
// avoid create session at the exact same time, leads to same random session users order
await new Promise(resolve => setTimeout(resolve, 1));
// sign in u2 in the same session // sign in u2 in the same session
await request(app.getHttpServer()) await app
.post('/api/auth/sign-in') .POST('/api/auth/sign-in')
.set('cookie', cookie) .send({ email: u2.email, password: u2.password })
.send({ email: u2.email, password: '3' })
.expect(200); .expect(200);
// list [u1, u2] // list [u1, u2]
const sessions = await request(app.getHttpServer()) const sessions = await app.GET('/api/auth/sessions').expect(200);
.get('/api/auth/sessions')
.set('cookie', cookie)
.expect(200);
t.is(sessions.body.users.length, 2); t.is(sessions.body.users.length, 2);
t.is(sessions.body.users[0].id, u1.id); t.like(
t.is(sessions.body.users[1].id, u2.id); sessions.body.users.map((u: any) => u.id),
[u1.id, u2.id]
);
// default to latest signed in user: u2 // default to latest signed in user: u2
let session = await request(app.getHttpServer()) let session = await app.GET('/api/auth/session').expect(200);
.get('/api/auth/session')
.set('cookie', cookie)
.expect(200);
t.is(session.body.user.id, u2.id); t.is(session.body.user.id, u2.id);
// switch to u1 // switch to u1
session = await request(app.getHttpServer()) session = await app
.get('/api/auth/session') .GET('/api/auth/session')
.set('cookie', `${cookie};${AuthService.userCookieName}=${u1.id}`) .set(
'Cookie',
Object.entries(cookies)
.map(([k, v]) => `${k}=${v}`)
.join('; ')
)
.expect(200); .expect(200);
t.is(session.body.user.id, u1.id); t.is(session.body.user.id, u1.id);
}); });
test('should be able to sign out multiple accounts in one session', async t => { test('should be able to sign out multiple accounts in one session', async t => {
const { app, u1, auth } = t.context; const { app } = t.context;
const u2 = await auth.signUp('u4@affine.pro', '4'); const u1 = await app.signup('u1@affine.pro');
const u2 = await app.signup('u2@affine.pro');
// sign in u1
const signInRes = await request(app.getHttpServer())
.post('/api/auth/sign-in')
.send({ email: u1.email, password: '1' })
.expect(200);
const cookie = sessionCookie(signInRes.headers);
await new Promise(resolve => setTimeout(resolve, 1));
// sign in u2 in the same session
await request(app.getHttpServer())
.post('/api/auth/sign-in')
.set('cookie', cookie)
.send({ email: u2.email, password: '4' })
.expect(200);
// sign out u2 // sign out u2
let signOut = await request(app.getHttpServer()) await app.GET(`/api/auth/sign-out?user_id=${u2.id}`).expect(200);
.get(`/api/auth/sign-out?user_id=${u2.id}`)
.set('cookie', `${cookie};${AuthService.userCookieName}=${u2.id}`)
.expect(200);
// auto switch to u1 after sign out u2
const userIdCookie = signOut.get('Set-Cookie')?.find(c => {
return c.startsWith(`${AuthService.userCookieName}=`);
});
t.true(userIdCookie?.startsWith(`${AuthService.userCookieName}=${u1.id}`));
// list [u1] // list [u1]
const session = await request(app.getHttpServer()) let session = await app.GET('/api/auth/session').expect(200);
.get('/api/auth/session')
.set('cookie', cookie)
.expect(200);
t.is(session.body.user.id, u1.id); t.is(session.body.user.id, u1.id);
// sign in u2 in the same session // sign in u2 in the same session
await request(app.getHttpServer()) await app
.post('/api/auth/sign-in') .POST('/api/auth/sign-in')
.set('cookie', cookie) .send({ email: u2.email, password: u2.password })
.send({ email: u2.email, password: '4' })
.expect(200); .expect(200);
// sign out all account in session // sign out all account in session
signOut = await request(app.getHttpServer()) await app.GET('/api/auth/sign-out').expect(200);
.get('/api/auth/sign-out')
.set('cookie', cookie)
.expect(200);
t.true( session = await app.GET('/api/auth/session').expect(200);
signOut t.falsy(session.body.user);
.get('Set-Cookie')
?.some(c => c.startsWith(`${AuthService.sessionCookieName}=;`))
);
t.true(
signOut
.get('Set-Cookie')
?.some(c => c.startsWith(`${AuthService.userCookieName}=;`))
);
}); });

View File

@@ -39,7 +39,7 @@ let u1!: CurrentUser;
let sessionId = ''; let sessionId = '';
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [AuthModule], imports: [AuthModule],
controllers: [TestController], controllers: [TestController],
}); });

View File

@@ -1,5 +1,3 @@
/// <reference types="../global.d.ts" />
import type { ExecutionContext, TestFn } from 'ava'; import type { ExecutionContext, TestFn } from 'ava';
import ava from 'ava'; import ava from 'ava';

View File

@@ -1,5 +1,3 @@
/// <reference types="../global.d.ts" />
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import type { TestFn } from 'ava'; import type { TestFn } from 'ava';
@@ -25,8 +23,8 @@ import {
createTestingApp, createTestingApp,
createWorkspace, createWorkspace,
inviteUser, inviteUser,
signUp,
TestingApp, TestingApp,
TestUser,
} from './utils'; } from './utils';
import { import {
array2sse, array2sse,
@@ -51,10 +49,11 @@ const test = ava as TestFn<{
prompt: PromptService; prompt: PromptService;
provider: CopilotProviderService; provider: CopilotProviderService;
storage: CopilotStorage; storage: CopilotStorage;
u1: TestUser;
}>; }>;
test.beforeEach(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
plugins: { plugins: {
@@ -87,12 +86,13 @@ test.beforeEach(async t => {
t.context.storage = storage; t.context.storage = storage;
}); });
let token: string;
const promptName = 'prompt'; const promptName = 'prompt';
test.beforeEach(async t => { test.beforeEach(async t => {
Sinon.restore();
const { app, prompt } = t.context; const { app, prompt } = t.context;
const user = await signUp(app, 'test', 'darksky@affine.pro', '123456'); await app.initTestingDB();
token = user.token.token; await prompt.onModuleInit();
t.context.u1 = await app.signup('u1@affine.pro');
unregisterCopilotProvider(OpenAIProvider.type); unregisterCopilotProvider(OpenAIProvider.type);
unregisterCopilotProvider(FalProvider.type); unregisterCopilotProvider(FalProvider.type);
@@ -104,14 +104,14 @@ test.beforeEach(async t => {
]); ]);
}); });
test.afterEach.always(async t => { test.after.always(async t => {
await t.context.app.close(); await t.context.app.close();
}); });
// ==================== session ==================== // ==================== session ====================
test('should create session correctly', async t => { test('should create session correctly', async t => {
const { app } = t.context; const { app, u1 } = t.context;
const assertCreateSession = async ( const assertCreateSession = async (
workspaceId: string, workspaceId: string,
@@ -121,12 +121,12 @@ test('should create session correctly', async t => {
} }
) => { ) => {
await asserter( await asserter(
createCopilotSession(app, token, workspaceId, randomUUID(), promptName) createCopilotSession(app, workspaceId, randomUUID(), promptName)
); );
}; };
{ {
const { id } = await createWorkspace(app, token); const { id } = await createWorkspace(app);
await assertCreateSession( await assertCreateSession(
id, id,
'should be able to create session with cloud workspace that user can access' 'should be able to create session with cloud workspace that user can access'
@@ -141,10 +141,9 @@ test('should create session correctly', async t => {
} }
{ {
const { const u2 = await app.createUser('u2@affine.pro');
token: { token }, const { id } = await createWorkspace(app);
} = await signUp(app, 'test', 'test@affine.pro', '123456'); await app.login(u2);
const { id } = await createWorkspace(app, token);
await assertCreateSession(id, '', async x => { await assertCreateSession(id, '', async x => {
await t.throwsAsync( await t.throwsAsync(
x, x,
@@ -153,7 +152,9 @@ test('should create session correctly', async t => {
); );
}); });
const inviteId = await inviteUser(app, token, id, 'darksky@affine.pro'); app.switchUser(u1);
const inviteId = await inviteUser(app, id, u2.email);
await app.login(u2);
await acceptInviteById(app, id, inviteId, false); await acceptInviteById(app, id, inviteId, false);
await assertCreateSession( await assertCreateSession(
id, id,
@@ -172,15 +173,14 @@ test('should update session correctly', async t => {
t.truthy(await x, error); t.truthy(await x, error);
} }
) => { ) => {
await asserter(updateCopilotSession(app, token, sessionId, promptName)); await asserter(updateCopilotSession(app, sessionId, promptName));
}; };
{ {
const { id: workspaceId } = await createWorkspace(app, token); const { id: workspaceId } = await createWorkspace(app);
const docId = randomUUID(); const docId = randomUUID();
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
workspaceId, workspaceId,
docId, docId,
promptName promptName
@@ -194,7 +194,6 @@ test('should update session correctly', async t => {
{ {
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
randomUUID(), randomUUID(),
randomUUID(), randomUUID(),
promptName promptName
@@ -206,19 +205,14 @@ test('should update session correctly', async t => {
} }
{ {
const aToken = (await signUp(app, 'test', 'test@affine.pro', '123456')) await app.signup('test@affine.pro');
.token.token; const u2 = await app.createUser('u2@affine.pro');
const { id: workspaceId } = await createWorkspace(app, aToken); const { id: workspaceId } = await createWorkspace(app);
const inviteId = await inviteUser( const inviteId = await inviteUser(app, workspaceId, u2.email);
app, await app.login(u2);
aToken,
workspaceId,
'darksky@affine.pro'
);
await acceptInviteById(app, workspaceId, inviteId, false); await acceptInviteById(app, workspaceId, inviteId, false);
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
workspaceId, workspaceId,
randomUUID(), randomUUID(),
promptName promptName
@@ -242,10 +236,9 @@ test('should update session correctly', async t => {
}); });
test('should fork session correctly', async t => { test('should fork session correctly', async t => {
const { app } = t.context; const { app, u1 } = t.context;
const assertForkSession = async ( const assertForkSession = async (
token: string,
workspaceId: string, workspaceId: string,
sessionId: string, sessionId: string,
lastMessageId: string, lastMessageId: string,
@@ -259,7 +252,6 @@ test('should fork session correctly', async t => {
await asserter( await asserter(
forkCopilotSession( forkCopilotSession(
app, app,
token,
workspaceId, workspaceId,
randomUUID(), randomUUID(),
sessionId, sessionId,
@@ -268,10 +260,9 @@ test('should fork session correctly', async t => {
); );
// prepare session // prepare session
const { id } = await createWorkspace(app, token); const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
id, id,
randomUUID(), randomUUID(),
promptName promptName
@@ -281,10 +272,10 @@ test('should fork session correctly', async t => {
// should be able to fork session // should be able to fork session
{ {
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const messageId = await createCopilotMessage(app, token, sessionId); const messageId = await createCopilotMessage(app, sessionId);
await chatWithText(app, token, sessionId, messageId); await chatWithText(app, sessionId, messageId);
} }
const histories = await getHistories(app, token, { workspaceId: id }); const histories = await getHistories(app, { workspaceId: id });
const latestMessageId = histories[0].messages.findLast( const latestMessageId = histories[0].messages.findLast(
m => m.role === 'assistant' m => m.role === 'assistant'
)?.id; )?.id;
@@ -292,7 +283,6 @@ test('should fork session correctly', async t => {
// should be able to fork session // should be able to fork session
forkedSessionId = await assertForkSession( forkedSessionId = await assertForkSession(
token,
id, id,
sessionId, sessionId,
latestMessageId!, latestMessageId!,
@@ -301,49 +291,36 @@ test('should fork session correctly', async t => {
} }
{ {
const { const u2 = await app.signup('u2@affine.pro');
token: { token: newToken }, await assertForkSession(id, sessionId, randomUUID(), '', async x => {
} = await signUp(app, 'test', 'test@affine.pro', '123456');
await assertForkSession(
newToken,
id,
sessionId,
randomUUID(),
'',
async x => {
await t.throwsAsync( await t.throwsAsync(
x, x,
{ instanceOf: Error }, { instanceOf: Error },
'should not able to fork session with cloud workspace that user cannot access' 'should not able to fork session with cloud workspace that user cannot access'
); );
} });
);
const inviteId = await inviteUser(app, token, id, 'test@affine.pro'); app.switchUser(u1);
const inviteId = await inviteUser(app, id, u2.email);
app.switchUser(u2);
await acceptInviteById(app, id, inviteId, false); await acceptInviteById(app, id, inviteId, false);
await assertForkSession( await assertForkSession(id, sessionId, randomUUID(), '', async x => {
newToken,
id,
sessionId,
randomUUID(),
'',
async x => {
await t.throwsAsync( await t.throwsAsync(
x, x,
{ instanceOf: Error }, { instanceOf: Error },
'should not able to fork a root session from other user' 'should not able to fork a root session from other user'
); );
} });
);
const histories = await getHistories(app, token, { workspaceId: id }); app.switchUser(u1);
const histories = await getHistories(app, { workspaceId: id });
const latestMessageId = histories const latestMessageId = histories
.find(h => h.sessionId === forkedSessionId) .find(h => h.sessionId === forkedSessionId)
?.messages.findLast(m => m.role === 'assistant')?.id; ?.messages.findLast(m => m.role === 'assistant')?.id;
t.truthy(latestMessageId, 'should find latest message id'); t.truthy(latestMessageId, 'should find latest message id');
app.switchUser(u2);
await assertForkSession( await assertForkSession(
newToken,
id, id,
forkedSessionId, forkedSessionId,
latestMessageId!, latestMessageId!,
@@ -355,9 +332,9 @@ test('should fork session correctly', async t => {
test('should be able to use test provider', async t => { test('should be able to use test provider', async t => {
const { app } = t.context; const { app } = t.context;
const { id } = await createWorkspace(app, token); const { id } = await createWorkspace(app);
t.truthy( t.truthy(
await createCopilotSession(app, token, id, randomUUID(), promptName), await createCopilotSession(app, id, randomUUID(), promptName),
'failed to create session' 'failed to create session'
); );
}); });
@@ -368,21 +345,20 @@ test('should create message correctly', async t => {
const { app } = t.context; const { app } = t.context;
{ {
const { id } = await createWorkspace(app, token); const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
id, id,
randomUUID(), randomUUID(),
promptName promptName
); );
const messageId = await createCopilotMessage(app, token, sessionId); const messageId = await createCopilotMessage(app, sessionId);
t.truthy(messageId, 'should be able to create message with valid session'); t.truthy(messageId, 'should be able to create message with valid session');
} }
{ {
await t.throwsAsync( await t.throwsAsync(
createCopilotMessage(app, token, randomUUID()), createCopilotMessage(app, randomUUID()),
{ instanceOf: Error }, { instanceOf: Error },
'should not able to create message with invalid session' 'should not able to create message with invalid session'
); );
@@ -396,26 +372,25 @@ test('should be able to chat with api', async t => {
Sinon.stub(storage, 'handleRemoteLink').resolvesArg(2); Sinon.stub(storage, 'handleRemoteLink').resolvesArg(2);
const { id } = await createWorkspace(app, token); const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
id, id,
randomUUID(), randomUUID(),
promptName promptName
); );
const messageId = await createCopilotMessage(app, token, sessionId); const messageId = await createCopilotMessage(app, sessionId);
const ret = await chatWithText(app, token, sessionId, messageId); const ret = await chatWithText(app, sessionId, messageId);
t.is(ret, 'generate text to text', 'should be able to chat with text'); t.is(ret, 'generate text to text', 'should be able to chat with text');
const ret2 = await chatWithTextStream(app, token, sessionId, messageId); const ret2 = await chatWithTextStream(app, sessionId, messageId);
t.is( t.is(
ret2, ret2,
textToEventStream('generate text to text stream', messageId), textToEventStream('generate text to text stream', messageId),
'should be able to chat with text stream' 'should be able to chat with text stream'
); );
const ret3 = await chatWithImages(app, token, sessionId, messageId); const ret3 = await chatWithImages(app, sessionId, messageId);
t.is( t.is(
array2sse(sse2array(ret3).filter(e => e.event !== 'event')), array2sse(sse2array(ret3).filter(e => e.event !== 'event')),
textToEventStream( textToEventStream(
@@ -432,21 +407,15 @@ test('should be able to chat with api', async t => {
test('should be able to chat with api by workflow', async t => { test('should be able to chat with api by workflow', async t => {
const { app } = t.context; const { app } = t.context;
const { id } = await createWorkspace(app, token); const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
id, id,
randomUUID(), randomUUID(),
'workflow:presentation' 'workflow:presentation'
); );
const messageId = await createCopilotMessage( const messageId = await createCopilotMessage(app, sessionId, 'apple company');
app, const ret = await chatWithWorkflow(app, sessionId, messageId);
token,
sessionId,
'apple company'
);
const ret = await chatWithWorkflow(app, token, sessionId, messageId);
t.is( t.is(
array2sse(sse2array(ret).filter(e => e.event !== 'event')), array2sse(sse2array(ret).filter(e => e.event !== 'event')),
textToEventStream(['generate text to text stream'], messageId), textToEventStream(['generate text to text stream'], messageId),
@@ -459,25 +428,20 @@ test('should be able to chat with special image model', async t => {
Sinon.stub(storage, 'handleRemoteLink').resolvesArg(2); Sinon.stub(storage, 'handleRemoteLink').resolvesArg(2);
const { id } = await createWorkspace(app, token); const { id } = await createWorkspace(app);
const testWithModel = async (promptName: string, finalPrompt: string) => { const testWithModel = async (promptName: string, finalPrompt: string) => {
const model = prompts.find(p => p.name === promptName)?.model; const model = prompts.find(p => p.name === promptName)?.model;
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
id, id,
randomUUID(), randomUUID(),
promptName promptName
); );
const messageId = await createCopilotMessage( const messageId = await createCopilotMessage(app, sessionId, 'some-tag', [
app, `https://example.com/${promptName}.jpg`,
token, ]);
sessionId, const ret3 = await chatWithImages(app, sessionId, messageId);
'some-tag',
[`https://example.com/${promptName}.jpg`]
);
const ret3 = await chatWithImages(app, token, sessionId, messageId);
t.is( t.is(
ret3, ret3,
textToEventStream( textToEventStream(
@@ -506,20 +470,19 @@ test('should be able to retry with api', async t => {
// normal chat // normal chat
{ {
const { id } = await createWorkspace(app, token); const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
id, id,
randomUUID(), randomUUID(),
promptName promptName
); );
const messageId = await createCopilotMessage(app, token, sessionId); const messageId = await createCopilotMessage(app, sessionId);
// chat 2 times // chat 2 times
await chatWithText(app, token, sessionId, messageId); await chatWithText(app, sessionId, messageId);
await chatWithText(app, token, sessionId, messageId); await chatWithText(app, sessionId, messageId);
const histories = await getHistories(app, token, { workspaceId: id }); const histories = await getHistories(app, { workspaceId: id });
t.deepEqual( t.deepEqual(
histories.map(h => h.messages.map(m => m.content)), histories.map(h => h.messages.map(m => m.content)),
[['generate text to text', 'generate text to text']], [['generate text to text', 'generate text to text']],
@@ -529,21 +492,20 @@ test('should be able to retry with api', async t => {
// retry chat // retry chat
{ {
const { id } = await createWorkspace(app, token); const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
id, id,
randomUUID(), randomUUID(),
promptName promptName
); );
const messageId = await createCopilotMessage(app, token, sessionId); const messageId = await createCopilotMessage(app, sessionId);
await chatWithText(app, token, sessionId, messageId); await chatWithText(app, sessionId, messageId);
// retry without message id // retry without message id
await chatWithText(app, token, sessionId); await chatWithText(app, sessionId);
// should only have 1 message // should only have 1 message
const histories = await getHistories(app, token, { workspaceId: id }); const histories = await getHistories(app, { workspaceId: id });
t.deepEqual( t.deepEqual(
histories.map(h => h.messages.map(m => m.content)), histories.map(h => h.messages.map(m => m.content)),
[['generate text to text']], [['generate text to text']],
@@ -557,40 +519,34 @@ test('should be able to retry with api', async t => {
test('should reject message from different session', async t => { test('should reject message from different session', async t => {
const { app } = t.context; const { app } = t.context;
const { id } = await createWorkspace(app, token); const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
id, id,
randomUUID(), randomUUID(),
promptName promptName
); );
const anotherSessionId = await createCopilotSession( const anotherSessionId = await createCopilotSession(
app, app,
token,
id, id,
randomUUID(), randomUUID(),
promptName promptName
); );
const anotherMessageId = await createCopilotMessage( const anotherMessageId = await createCopilotMessage(app, anotherSessionId);
app,
token,
anotherSessionId
);
await t.throwsAsync( await t.throwsAsync(
chatWithText(app, token, sessionId, anotherMessageId), chatWithText(app, sessionId, anotherMessageId),
{ instanceOf: Error }, { instanceOf: Error },
'should reject message from different session' 'should reject message from different session'
); );
}); });
test('should reject request from different user', async t => { test('should reject request from different user', async t => {
const { app } = t.context; const { app, u1 } = t.context;
const { id } = await createWorkspace(app, token); const u2 = await app.createUser('u2@affine.pro');
const { id } = await createWorkspace(app);
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
id, id,
randomUUID(), randomUUID(),
promptName promptName
@@ -598,9 +554,9 @@ test('should reject request from different user', async t => {
// should reject message from different user // should reject message from different user
{ {
const { token } = await signUp(app, 'a1', 'a1@affine.pro', '123456'); await app.login(u2);
await t.throwsAsync( await t.throwsAsync(
createCopilotMessage(app, token.token, sessionId), createCopilotMessage(app, sessionId),
{ instanceOf: Error }, { instanceOf: Error },
'should reject message from different user' 'should reject message from different user'
); );
@@ -608,11 +564,12 @@ test('should reject request from different user', async t => {
// should reject chat from different user // should reject chat from different user
{ {
const messageId = await createCopilotMessage(app, token, sessionId); app.switchUser(u1);
const messageId = await createCopilotMessage(app, sessionId);
{ {
const { token } = await signUp(app, 'a2', 'a2@affine.pro', '123456'); app.switchUser(u2);
await t.throwsAsync( await t.throwsAsync(
chatWithText(app, token.token, sessionId, messageId), chatWithText(app, sessionId, messageId),
{ instanceOf: Error }, { instanceOf: Error },
'should reject chat from different user' 'should reject chat from different user'
); );
@@ -625,20 +582,19 @@ test('should reject request from different user', async t => {
test('should be able to list history', async t => { test('should be able to list history', async t => {
const { app } = t.context; const { app } = t.context;
const { id: workspaceId } = await createWorkspace(app, token); const { id: workspaceId } = await createWorkspace(app);
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
token,
workspaceId, workspaceId,
randomUUID(), randomUUID(),
promptName promptName
); );
const messageId = await createCopilotMessage(app, token, sessionId, 'hello'); const messageId = await createCopilotMessage(app, sessionId, 'hello');
await chatWithText(app, token, sessionId, messageId); await chatWithText(app, sessionId, messageId);
{ {
const histories = await getHistories(app, token, { workspaceId }); const histories = await getHistories(app, { workspaceId });
t.deepEqual( t.deepEqual(
histories.map(h => h.messages.map(m => m.content)), histories.map(h => h.messages.map(m => m.content)),
[['hello', 'generate text to text']], [['hello', 'generate text to text']],
@@ -647,7 +603,7 @@ test('should be able to list history', async t => {
} }
{ {
const histories = await getHistories(app, token, { const histories = await getHistories(app, {
workspaceId, workspaceId,
options: { messageOrder: 'desc' }, options: { messageOrder: 'desc' },
}); });
@@ -660,17 +616,16 @@ test('should be able to list history', async t => {
}); });
test('should reject request that user have not permission', async t => { test('should reject request that user have not permission', async t => {
const { app } = t.context; const { app, u1 } = t.context;
const { const u2 = await app.createUser('u2@affine.pro');
token: { token: anotherToken }, const { id: workspaceId } = await createWorkspace(app);
} = await signUp(app, 'a1', 'a1@affine.pro', '123456');
const { id: workspaceId } = await createWorkspace(app, anotherToken);
// should reject request that user have not permission // should reject request that user have not permission
{ {
await app.login(u2);
await t.throwsAsync( await t.throwsAsync(
getHistories(app, token, { workspaceId }), getHistories(app, { workspaceId }),
{ instanceOf: Error }, { instanceOf: Error },
'should reject request that user have not permission' 'should reject request that user have not permission'
); );
@@ -678,16 +633,13 @@ test('should reject request that user have not permission', async t => {
// should able to list history after user have permission // should able to list history after user have permission
{ {
const inviteId = await inviteUser( app.switchUser(u1);
app, const inviteId = await inviteUser(app, workspaceId, u2.email);
anotherToken, app.switchUser(u2);
workspaceId,
'darksky@affine.pro'
);
await acceptInviteById(app, workspaceId, inviteId, false); await acceptInviteById(app, workspaceId, inviteId, false);
t.deepEqual( t.deepEqual(
await getHistories(app, token, { workspaceId }), await getHistories(app, { workspaceId }),
[], [],
'should able to list history after user have permission' 'should able to list history after user have permission'
); );
@@ -696,24 +648,24 @@ test('should reject request that user have not permission', async t => {
{ {
const sessionId = await createCopilotSession( const sessionId = await createCopilotSession(
app, app,
anotherToken,
workspaceId, workspaceId,
randomUUID(), randomUUID(),
promptName promptName
); );
const messageId = await createCopilotMessage(app, anotherToken, sessionId); const messageId = await createCopilotMessage(app, sessionId);
await chatWithText(app, anotherToken, sessionId, messageId); await chatWithText(app, sessionId, messageId);
const histories = await getHistories(app, anotherToken, { workspaceId }); const histories = await getHistories(app, { workspaceId });
t.deepEqual( t.deepEqual(
histories.map(h => h.messages.map(m => m.content)), histories.map(h => h.messages.map(m => m.content)),
[['generate text to text']], [['generate text to text']],
'should able to list history' 'should able to list history'
); );
app.switchUser(u1);
t.deepEqual( t.deepEqual(
await getHistories(app, token, { workspaceId }), await getHistories(app, { workspaceId }),
[], [],
'should not list history created by another user' 'should not list history created by another user'
); );
@@ -723,6 +675,6 @@ test('should reject request that user have not permission', async t => {
test('should be able to search image from unsplash', async t => { test('should be able to search image from unsplash', async t => {
const { app } = t.context; const { app } = t.context;
const resp = await unsplashSearch(app, token); const resp = await unsplashSearch(app);
t.not(resp.status, 404, 'route should be exists'); t.not(resp.status, 404, 'route should be exists');
}); });

View File

@@ -55,8 +55,9 @@ const test = ava as TestFn<{
json: CopilotCheckJsonExecutor; json: CopilotCheckJsonExecutor;
}; };
}>; }>;
let userId: string;
test.beforeEach(async t => { test.before(async t => {
const module = await createTestingModule({ const module = await createTestingModule({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
@@ -99,17 +100,19 @@ test.beforeEach(async t => {
}; };
}); });
test.afterEach.always(async t => {
await t.context.module.close();
});
let userId: string;
test.beforeEach(async t => { test.beforeEach(async t => {
const { auth } = t.context; Sinon.restore();
const { module, auth, prompt } = t.context;
await module.initTestingDB();
await prompt.onModuleInit();
const user = await auth.signUp('test@affine.pro', '123456'); const user = await auth.signUp('test@affine.pro', '123456');
userId = user.id; userId = user.id;
}); });
test.after.always(async t => {
await t.context.module.close();
});
// ==================== prompt ==================== // ==================== prompt ====================
test('should be able to manage prompt', async t => { test('should be able to manage prompt', async t => {

View File

@@ -3,7 +3,6 @@ import path from 'node:path';
import { Package } from '@affine-tools/utils/workspace'; import { Package } from '@affine-tools/utils/workspace';
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import type { TestFn } from 'ava'; import type { TestFn } from 'ava';
import ava from 'ava'; import ava from 'ava';
import request from 'supertest'; import request from 'supertest';
@@ -13,7 +12,6 @@ import { createTestingApp } from '../utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
app: INestApplication; app: INestApplication;
db: PrismaClient;
}>; }>;
const mobileUAString = const mobileUAString =
@@ -51,12 +49,11 @@ test.before('init selfhost server', async t => {
const staticPath = new Package('@affine/server').join('static').value; const staticPath = new Package('@affine/server').join('static').value;
initTestStaticFiles(staticPath); initTestStaticFiles(staticPath);
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [DocRendererModule], imports: [DocRendererModule],
}); });
t.context.app = app; t.context.app = app;
t.context.db = t.context.app.get(PrismaClient);
}); });
test.after.always(async t => { test.after.always(async t => {

View File

@@ -1,23 +1,24 @@
import type { INestApplication } from '@nestjs/common';
import type { TestFn } from 'ava'; import type { TestFn } from 'ava';
import ava from 'ava'; import ava from 'ava';
import Sinon from 'sinon'; import Sinon from 'sinon';
import { AppModule } from '../app.module';
import { MailService } from '../base/mailer'; import { MailService } from '../base/mailer';
import { createTestingApp, createWorkspace, inviteUser, signUp } from './utils'; import {
createTestingApp,
createWorkspace,
inviteUser,
TestingApp,
} from './utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
app: INestApplication; app: TestingApp;
mail: MailService; mail: MailService;
}>; }>;
import * as renderers from '../mails'; import * as renderers from '../mails';
test.beforeEach(async t => { test.beforeEach(async t => {
const { module, app } = await createTestingApp({ const app = await createTestingApp();
imports: [AppModule],
});
const mail = module.get(MailService); const mail = app.get(MailService);
t.context.app = app; t.context.app = app;
t.context.mail = mail; t.context.mail = mail;
}); });
@@ -30,14 +31,12 @@ test('should send invite email', async t => {
const { mail, app } = t.context; const { mail, app } = t.context;
if (mail.hasConfigured()) { if (mail.hasConfigured()) {
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); const u2 = await app.signup('u2@affine.pro');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); const u1 = await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token);
const stub = Sinon.stub(mail, 'send'); const stub = Sinon.stub(mail, 'send');
await inviteUser(app, u1.token.token, workspace.id, u2.email, true); const workspace = await createWorkspace(app);
await inviteUser(app, workspace.id, u2.email, true);
t.true(stub.calledOnce); t.true(stub.calledOnce);

View File

@@ -106,7 +106,7 @@ function gql(app: INestApplication, query: string) {
} }
test.before(async ({ context }) => { test.before(async ({ context }) => {
const { app } = await createTestingApp({ const app = await createTestingApp({
providers: [TestResolver, TestGateway], providers: [TestResolver, TestGateway],
controllers: [TestController], controllers: [TestController],
}); });

View File

@@ -3,7 +3,7 @@ import '../../plugins/config';
import { Controller, Get, HttpStatus, UseGuards } from '@nestjs/common'; import { Controller, Get, HttpStatus, UseGuards } from '@nestjs/common';
import ava, { TestFn } from 'ava'; import ava, { TestFn } from 'ava';
import Sinon from 'sinon'; import Sinon from 'sinon';
import request, { type Response } from 'supertest'; import { type Response } from 'supertest';
import { AppModule } from '../../app.module'; import { AppModule } from '../../app.module';
import { ConfigModule } from '../../base/config'; import { ConfigModule } from '../../base/config';
@@ -13,12 +13,11 @@ import {
Throttle, Throttle,
ThrottlerStorage, ThrottlerStorage,
} from '../../base/throttler'; } from '../../base/throttler';
import { AuthService, Public } from '../../core/auth'; import { Public } from '../../core/auth';
import { createTestingApp, internalSignIn, TestingApp } from '../utils'; import { createTestingApp, TestingApp } from '../utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
storage: ThrottlerStorage; storage: ThrottlerStorage;
cookie: string;
app: TestingApp; app: TestingApp;
}>; }>;
@@ -88,7 +87,7 @@ class NonThrottledController {
} }
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
throttler: { throttler: {
@@ -108,11 +107,8 @@ test.before(async t => {
}); });
test.beforeEach(async t => { test.beforeEach(async t => {
await t.context.app.initTestingDB();
const { app } = t.context; const { app } = t.context;
const auth = app.get(AuthService); await app.initTestingDB();
const u1 = await auth.signUp('u1@affine.pro', 'test');
t.context.cookie = await internalSignIn(app, u1.id);
}); });
test.after.always(async t => { test.after.always(async t => {
@@ -137,8 +133,8 @@ test('should be able to prevent requests if limit is reached', async t => {
isBlocked: true, isBlocked: true,
timeToBlockExpire: 10, timeToBlockExpire: 10,
}); });
const res = await request(app.getHttpServer()) const res = await app
.get('/nonthrottled/strict') .GET('/nonthrottled/strict')
.expect(HttpStatus.TOO_MANY_REQUESTS); .expect(HttpStatus.TOO_MANY_REQUESTS);
const headers = rateLimitHeaders(res); const headers = rateLimitHeaders(res);
@@ -152,9 +148,7 @@ test('should be able to prevent requests if limit is reached', async t => {
test('should use default throttler for unauthenticated user when not specified', async t => { test('should use default throttler for unauthenticated user when not specified', async t => {
const { app } = t.context; const { app } = t.context;
const res = await request(app.getHttpServer()) const res = await app.GET('/nonthrottled/default').expect(200);
.get('/nonthrottled/default')
.expect(200);
const headers = rateLimitHeaders(res); const headers = rateLimitHeaders(res);
@@ -165,9 +159,7 @@ test('should use default throttler for unauthenticated user when not specified',
test('should skip throttler for unauthenticated user when specified', async t => { test('should skip throttler for unauthenticated user when specified', async t => {
const { app } = t.context; const { app } = t.context;
let res = await request(app.getHttpServer()) let res = await app.GET('/nonthrottled/skip').expect(200);
.get('/nonthrottled/skip')
.expect(200);
let headers = rateLimitHeaders(res); let headers = rateLimitHeaders(res);
@@ -175,7 +167,7 @@ test('should skip throttler for unauthenticated user when specified', async t =>
t.is(headers.remaining, undefined!); t.is(headers.remaining, undefined!);
t.is(headers.reset, undefined!); t.is(headers.reset, undefined!);
res = await request(app.getHttpServer()).get('/throttled/skip').expect(200); res = await app.GET('/throttled/skip').expect(200);
headers = rateLimitHeaders(res); headers = rateLimitHeaders(res);
@@ -187,9 +179,7 @@ test('should skip throttler for unauthenticated user when specified', async t =>
test('should use specified throttler for unauthenticated user', async t => { test('should use specified throttler for unauthenticated user', async t => {
const { app } = t.context; const { app } = t.context;
const res = await request(app.getHttpServer()) const res = await app.GET('/nonthrottled/strict').expect(200);
.get('/nonthrottled/strict')
.expect(200);
const headers = rateLimitHeaders(res); const headers = rateLimitHeaders(res);
@@ -199,12 +189,10 @@ test('should use specified throttler for unauthenticated user', async t => {
// ==== authenticated user visits ==== // ==== authenticated user visits ====
test('should not protect unspecified routes', async t => { test('should not protect unspecified routes', async t => {
const { app, cookie } = t.context; const { app } = t.context;
const res = await request(app.getHttpServer()) await app.signup('u1@affine.pro');
.get('/nonthrottled/default') const res = await app.GET('/nonthrottled/default').expect(200);
.set('Cookie', cookie)
.expect(200);
const headers = rateLimitHeaders(res); const headers = rateLimitHeaders(res);
@@ -214,12 +202,10 @@ test('should not protect unspecified routes', async t => {
}); });
test('should use default throttler for authenticated user when not specified', async t => { test('should use default throttler for authenticated user when not specified', async t => {
const { app, cookie } = t.context; const { app } = t.context;
const res = await request(app.getHttpServer()) await app.signup('u1@affine.pro');
.get('/throttled/default') const res = await app.GET('/throttled/default').expect(200);
.set('Cookie', cookie)
.expect(200);
const headers = rateLimitHeaders(res); const headers = rateLimitHeaders(res);
@@ -228,22 +214,17 @@ test('should use default throttler for authenticated user when not specified', a
}); });
test('should use same throttler for multiple routes', async t => { test('should use same throttler for multiple routes', async t => {
const { app, cookie } = t.context; const { app } = t.context;
let res = await request(app.getHttpServer()) await app.signup('u1@affine.pro');
.get('/throttled/default') let res = await app.GET('/throttled/default').expect(200);
.set('Cookie', cookie)
.expect(200);
let headers = rateLimitHeaders(res); let headers = rateLimitHeaders(res);
t.is(headers.limit, '120'); t.is(headers.limit, '120');
t.is(headers.remaining, '119'); t.is(headers.remaining, '119');
res = await request(app.getHttpServer()) res = await app.GET('/throttled/default2').expect(200);
.get('/throttled/default2')
.set('Cookie', cookie)
.expect(200);
headers = rateLimitHeaders(res); headers = rateLimitHeaders(res);
@@ -252,22 +233,17 @@ test('should use same throttler for multiple routes', async t => {
}); });
test('should use different throttler if specified', async t => { test('should use different throttler if specified', async t => {
const { app, cookie } = t.context; const { app } = t.context;
let res = await request(app.getHttpServer()) await app.signup('u1@affine.pro');
.get('/throttled/default') let res = await app.GET('/throttled/default').expect(200);
.set('Cookie', cookie)
.expect(200);
let headers = rateLimitHeaders(res); let headers = rateLimitHeaders(res);
t.is(headers.limit, '120'); t.is(headers.limit, '120');
t.is(headers.remaining, '119'); t.is(headers.remaining, '119');
res = await request(app.getHttpServer()) res = await app.GET('/throttled/default3').expect(200);
.get('/throttled/default3')
.set('Cookie', cookie)
.expect(200);
headers = rateLimitHeaders(res); headers = rateLimitHeaders(res);
@@ -276,12 +252,10 @@ test('should use different throttler if specified', async t => {
}); });
test('should skip throttler for authenticated if `authenticated` throttler used', async t => { test('should skip throttler for authenticated if `authenticated` throttler used', async t => {
const { app, cookie } = t.context; const { app } = t.context;
const res = await request(app.getHttpServer()) await app.signup('u1@affine.pro');
.get('/throttled/authenticated') const res = await app.GET('/throttled/authenticated').expect(200);
.set('Cookie', cookie)
.expect(200);
const headers = rateLimitHeaders(res); const headers = rateLimitHeaders(res);
@@ -290,12 +264,10 @@ test('should skip throttler for authenticated if `authenticated` throttler used'
t.is(headers.reset, undefined!); t.is(headers.reset, undefined!);
}); });
test('should apply `default` throttler for authenticated user if `authenticated` throttler used', async t => { test('should apply `default` throttler for unauthenticated user if `authenticated` throttler used', async t => {
const { app } = t.context; const { app } = t.context;
const res = await request(app.getHttpServer()) const res = await app.GET('/throttled/authenticated').expect(200);
.get('/throttled/authenticated')
.expect(200);
const headers = rateLimitHeaders(res); const headers = rateLimitHeaders(res);
@@ -304,12 +276,10 @@ test('should apply `default` throttler for authenticated user if `authenticated`
}); });
test('should skip throttler for authenticated user when specified', async t => { test('should skip throttler for authenticated user when specified', async t => {
const { app, cookie } = t.context; const { app } = t.context;
const res = await request(app.getHttpServer()) await app.signup('u1@affine.pro');
.get('/throttled/skip') const res = await app.GET('/throttled/skip').expect(200);
.set('Cookie', cookie)
.expect(200);
const headers = rateLimitHeaders(res); const headers = rateLimitHeaders(res);
@@ -319,12 +289,10 @@ test('should skip throttler for authenticated user when specified', async t => {
}); });
test('should use specified throttler for authenticated user', async t => { test('should use specified throttler for authenticated user', async t => {
const { app, cookie } = t.context; const { app } = t.context;
const res = await request(app.getHttpServer()) await app.signup('u1@affine.pro');
.get('/throttled/strict') const res = await app.GET('/throttled/strict').expect(200);
.set('Cookie', cookie)
.expect(200);
const headers = rateLimitHeaders(res); const headers = rateLimitHeaders(res);
@@ -333,15 +301,13 @@ test('should use specified throttler for authenticated user', async t => {
}); });
test('should separate anonymous and authenticated user throttlers', async t => { test('should separate anonymous and authenticated user throttlers', async t => {
const { app, cookie } = t.context; const { app } = t.context;
const authenticatedUserRes = await request(app.getHttpServer()) const unauthenticatedUserRes = await app
.get('/throttled/default') .GET('/nonthrottled/default')
.set('Cookie', cookie)
.expect(200);
const unauthenticatedUserRes = await request(app.getHttpServer())
.get('/nonthrottled/default')
.expect(200); .expect(200);
await app.signup('u1@affine.pro');
const authenticatedUserRes = await app.GET('/throttled/default').expect(200);
const authenticatedResHeaders = rateLimitHeaders(authenticatedUserRes); const authenticatedResHeaders = rateLimitHeaders(authenticatedUserRes);
const unauthenticatedResHeaders = rateLimitHeaders(unauthenticatedUserRes); const unauthenticatedResHeaders = rateLimitHeaders(unauthenticatedUserRes);

View File

@@ -4,7 +4,6 @@ import { HttpStatus } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import ava, { TestFn } from 'ava'; import ava, { TestFn } from 'ava';
import Sinon from 'sinon'; import Sinon from 'sinon';
import request from 'supertest';
import { AppModule } from '../../app.module'; import { AppModule } from '../../app.module';
import { URLHelper } from '../../base'; import { URLHelper } from '../../base';
@@ -15,7 +14,7 @@ import { Models } from '../../models';
import { OAuthProviderName } from '../../plugins/oauth/config'; import { OAuthProviderName } from '../../plugins/oauth/config';
import { GoogleOAuthProvider } from '../../plugins/oauth/providers/google'; import { GoogleOAuthProvider } from '../../plugins/oauth/providers/google';
import { OAuthService } from '../../plugins/oauth/service'; import { OAuthService } from '../../plugins/oauth/service';
import { createTestingApp, getSession, TestingApp } from '../utils'; import { createTestingApp, currentUser, TestingApp } from '../utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
auth: AuthService; auth: AuthService;
@@ -27,7 +26,7 @@ const test = ava as TestFn<{
}>; }>;
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
plugins: { plugins: {
@@ -65,8 +64,8 @@ test.after.always(async t => {
test("should be able to redirect to oauth provider's login page", async t => { test("should be able to redirect to oauth provider's login page", async t => {
const { app } = t.context; const { app } = t.context;
const res = await request(app.getHttpServer()) const res = await app
.post('/api/oauth/preflight') .POST('/api/oauth/preflight')
.send({ provider: 'Google' }) .send({ provider: 'Google' })
.expect(HttpStatus.OK); .expect(HttpStatus.OK);
@@ -89,8 +88,8 @@ test("should be able to redirect to oauth provider's login page", async t => {
test('should throw if provider is invalid', async t => { test('should throw if provider is invalid', async t => {
const { app } = t.context; const { app } = t.context;
await request(app.getHttpServer()) await app
.post('/api/oauth/preflight') .POST('/api/oauth/preflight')
.send({ provider: 'Invalid' }) .send({ provider: 'Invalid' })
.expect(HttpStatus.BAD_REQUEST) .expect(HttpStatus.BAD_REQUEST)
.expect({ .expect({
@@ -129,8 +128,8 @@ test('should be able to get registered oauth providers', async t => {
test('should throw if code is missing in callback uri', async t => { test('should throw if code is missing in callback uri', async t => {
const { app } = t.context; const { app } = t.context;
await request(app.getHttpServer()) await app
.post('/api/oauth/callback') .POST('/api/oauth/callback')
.send({}) .send({})
.expect(HttpStatus.BAD_REQUEST) .expect(HttpStatus.BAD_REQUEST)
.expect({ .expect({
@@ -148,8 +147,8 @@ test('should throw if code is missing in callback uri', async t => {
test('should throw if state is missing in callback uri', async t => { test('should throw if state is missing in callback uri', async t => {
const { app } = t.context; const { app } = t.context;
await request(app.getHttpServer()) await app
.post('/api/oauth/callback') .POST('/api/oauth/callback')
.send({ code: '1' }) .send({ code: '1' })
.expect(HttpStatus.BAD_REQUEST) .expect(HttpStatus.BAD_REQUEST)
.expect({ .expect({
@@ -168,8 +167,8 @@ test('should throw if state is expired', async t => {
const { app, oauth } = t.context; const { app, oauth } = t.context;
Sinon.stub(oauth, 'isValidState').resolves(true); Sinon.stub(oauth, 'isValidState').resolves(true);
await request(app.getHttpServer()) await app
.post('/api/oauth/callback') .POST('/api/oauth/callback')
.send({ code: '1', state: '1' }) .send({ code: '1', state: '1' })
.expect(HttpStatus.BAD_REQUEST) .expect(HttpStatus.BAD_REQUEST)
.expect({ .expect({
@@ -186,8 +185,8 @@ test('should throw if state is expired', async t => {
test('should throw if state is invalid', async t => { test('should throw if state is invalid', async t => {
const { app } = t.context; const { app } = t.context;
await request(app.getHttpServer()) await app
.post('/api/oauth/callback') .POST('/api/oauth/callback')
.send({ code: '1', state: '1' }) .send({ code: '1', state: '1' })
.expect(HttpStatus.BAD_REQUEST) .expect(HttpStatus.BAD_REQUEST)
.expect({ .expect({
@@ -208,8 +207,8 @@ test('should throw if provider is missing in state', async t => {
Sinon.stub(oauth, 'getOAuthState').resolves({}); Sinon.stub(oauth, 'getOAuthState').resolves({});
Sinon.stub(oauth, 'isValidState').resolves(true); Sinon.stub(oauth, 'isValidState').resolves(true);
await request(app.getHttpServer()) await app
.post('/api/oauth/callback') .POST('/api/oauth/callback')
.send({ code: '1', state: '1' }) .send({ code: '1', state: '1' })
.expect(HttpStatus.BAD_REQUEST) .expect(HttpStatus.BAD_REQUEST)
.expect({ .expect({
@@ -231,8 +230,8 @@ test('should throw if provider is invalid in callback uri', async t => {
Sinon.stub(oauth, 'getOAuthState').resolves({ provider: 'Invalid' }); Sinon.stub(oauth, 'getOAuthState').resolves({ provider: 'Invalid' });
Sinon.stub(oauth, 'isValidState').resolves(true); Sinon.stub(oauth, 'isValidState').resolves(true);
await request(app.getHttpServer()) await app
.post('/api/oauth/callback') .POST('/api/oauth/callback')
.send({ code: '1', state: '1' }) .send({ code: '1', state: '1' })
.expect(HttpStatus.BAD_REQUEST) .expect(HttpStatus.BAD_REQUEST)
.expect({ .expect({
@@ -270,15 +269,15 @@ test('should be able to sign up with oauth', async t => {
mockOAuthProvider(app, 'u2@affine.pro'); mockOAuthProvider(app, 'u2@affine.pro');
const res = await request(app.getHttpServer()) await app
.post(`/api/oauth/callback`) .POST('/api/oauth/callback')
.send({ code: '1', state: '1' }) .send({ code: '1', state: '1' })
.expect(HttpStatus.OK); .expect(HttpStatus.OK);
const session = await getSession(app, res); const sessionUser = await currentUser(app);
t.truthy(session.user); t.truthy(sessionUser);
t.is(session.user!.email, 'u2@affine.pro'); t.is(sessionUser!.email, 'u2@affine.pro');
const user = await db.user.findFirst({ const user = await db.user.findFirst({
select: { select: {
@@ -300,8 +299,8 @@ test('should not throw if account registered', async t => {
mockOAuthProvider(app, u1.email); mockOAuthProvider(app, u1.email);
const res = await request(app.getHttpServer()) const res = await app
.post(`/api/oauth/callback`) .POST('/api/oauth/callback')
.send({ code: '1', state: '1' }) .send({ code: '1', state: '1' })
.expect(HttpStatus.OK); .expect(HttpStatus.OK);
@@ -309,25 +308,18 @@ test('should not throw if account registered', async t => {
}); });
test('should be able to fullfil user with oauth sign in', async t => { test('should be able to fullfil user with oauth sign in', async t => {
const { app, models, db } = t.context; const { app, db } = t.context;
const u3 = await models.user.create({ const u3 = await app.createUser('u3@affine.pro');
name: 'u3',
email: 'u3@affine.pro',
registered: false,
});
mockOAuthProvider(app, u3.email); mockOAuthProvider(app, u3.email);
const res = await request(app.getHttpServer()) await app.POST('/api/oauth/callback').send({ code: '1', state: '1' });
.post('/api/oauth/callback')
.send({ code: '1', state: '1' })
.expect(HttpStatus.OK);
const session = await getSession(app, res); const sessionUser = await currentUser(app);
t.truthy(session.user); t.truthy(sessionUser);
t.is(session.user!.email, u3.email); t.is(sessionUser!.email, u3.email);
const account = await db.connectedAccount.findFirst({ const account = await db.connectedAccount.findFirst({
where: { where: {

View File

@@ -182,7 +182,7 @@ function getLastCheckoutPrice(checkoutStub: Sinon.SinonStub) {
} }
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
plugins: { plugins: {

View File

@@ -1,14 +1,11 @@
/// <reference types="../global.d.ts" />
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { getCurrentMailMessageCount } from '@affine-test/kit/utils/cloud'; import { getCurrentMailMessageCount } from '@affine-test/kit/utils/cloud';
import { WorkspaceMemberStatus } from '@prisma/client'; import { User, WorkspaceMemberStatus } from '@prisma/client';
import type { TestFn } from 'ava'; import type { TestFn } from 'ava';
import ava from 'ava'; import ava from 'ava';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import Sinon from 'sinon'; import Sinon from 'sinon';
import request from 'supertest';
import { AppModule } from '../app.module'; import { AppModule } from '../app.module';
import { EventBus } from '../base'; import { EventBus } from '../base';
@@ -36,11 +33,9 @@ import {
revokeInviteLink, revokeInviteLink,
revokeMember, revokeMember,
revokeUser, revokeUser,
signUp,
sleep, sleep,
TestingApp, TestingApp,
updateDocDefaultRole, updateDocDefaultRole,
UserAuthedType,
} from './utils'; } from './utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
@@ -52,7 +47,7 @@ const test = ava as TestFn<{
}>; }>;
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [AppModule], imports: [AppModule],
tapModule: module => { tapModule: module => {
module module
@@ -89,19 +84,14 @@ const init = async (
memberLimit = 10, memberLimit = 10,
prefix = randomUUID() prefix = randomUUID()
) => { ) => {
const owner = await signUp( const owner = await app.signup(`${prefix}owner@affine.pro`);
app,
'owner',
`${prefix}owner@affine.pro`,
'123456'
);
const models = app.get(Models); const models = app.get(Models);
{ {
await models.userFeature.add(owner.id, 'pro_plan_v1', 'test'); await models.userFeature.add(owner.id, 'pro_plan_v1', 'test');
} }
const workspace = await createWorkspace(app, owner.token.token); const workspace = await createWorkspace(app);
const teamWorkspace = await createWorkspace(app, owner.token.token); const teamWorkspace = await createWorkspace(app);
{ {
models.workspaceFeature.add(teamWorkspace.id, 'team_plan_v1', 'test', { models.workspaceFeature.add(teamWorkspace.id, 'team_plan_v1', 'test', {
memberLimit, memberLimit,
@@ -113,37 +103,34 @@ const init = async (
permission: WorkspaceRole = WorkspaceRole.Collaborator, permission: WorkspaceRole = WorkspaceRole.Collaborator,
shouldSendEmail: boolean = false shouldSendEmail: boolean = false
) => { ) => {
const member = await signUp(app, email.split('@')[0], email, '123456'); const member = await app.signup(email);
{ {
// normal workspace // normal workspace
app.switchUser(owner);
const inviteId = await inviteUser( const inviteId = await inviteUser(
app, app,
owner.token.token,
workspace.id, workspace.id,
member.email, member.email,
shouldSendEmail shouldSendEmail
); );
app.switchUser(member);
await acceptInviteById(app, workspace.id, inviteId, shouldSendEmail); await acceptInviteById(app, workspace.id, inviteId, shouldSendEmail);
} }
{ {
// team workspace // team workspace
app.switchUser(owner);
const inviteId = await inviteUser( const inviteId = await inviteUser(
app, app,
owner.token.token,
teamWorkspace.id, teamWorkspace.id,
member.email, member.email,
shouldSendEmail shouldSendEmail
); );
app.switchUser(member);
await acceptInviteById(app, teamWorkspace.id, inviteId, shouldSendEmail); await acceptInviteById(app, teamWorkspace.id, inviteId, shouldSendEmail);
await grantMember( app.switchUser(owner);
app, await grantMember(app, teamWorkspace.id, member.id, permission);
owner.token.token,
teamWorkspace.id,
member.id,
permission
);
} }
return member; return member;
@@ -155,12 +142,13 @@ const init = async (
) => { ) => {
const members = []; const members = [];
for (const email of emails) { for (const email of emails) {
const member = await signUp(app, email.split('@')[0], email, '123456'); const member = await app.signup(email);
members.push(member); members.push(member);
} }
app.switchUser(owner);
const invites = await inviteUsers( const invites = await inviteUsers(
app, app,
owner.token.token,
teamWorkspace.id, teamWorkspace.id,
emails, emails,
shouldSendEmail shouldSendEmail
@@ -169,31 +157,19 @@ const init = async (
}; };
const getCreateInviteLinkFetcher = async (ws: WorkspaceType) => { const getCreateInviteLinkFetcher = async (ws: WorkspaceType) => {
const { link } = await createInviteLink( app.switchUser(owner);
app, const { link } = await createInviteLink(app, ws.id, 'OneDay');
owner.token.token,
ws.id,
'OneDay'
);
const inviteId = link.split('/').pop()!; const inviteId = link.split('/').pop()!;
return [ return [
inviteId, inviteId,
async ( async (email: string, shouldSendEmail: boolean = false) => {
email: string, const member = await app.signup(email);
shouldSendEmail: boolean = false await acceptInviteById(app, ws.id, inviteId, shouldSendEmail);
): Promise<UserAuthedType> => {
const member = await signUp(app, email.split('@')[0], email, '123456');
await acceptInviteById(
app,
ws.id,
inviteId,
shouldSendEmail,
member.token.token
);
return member; return member;
}, },
async (token: string) => { async (userId: string) => {
await acceptInviteById(app, ws.id, inviteId, false, token); app.switchUser(userId);
await acceptInviteById(app, ws.id, inviteId, false);
}, },
] as const; ] as const;
}; };
@@ -210,6 +186,7 @@ const init = async (
WorkspaceRole.External WorkspaceRole.External
); );
app.switchUser(owner.id);
return { return {
invite, invite,
inviteBatch, inviteBatch,
@@ -230,13 +207,15 @@ test('should be able to invite multiple users', async t => {
{ {
// no permission // no permission
app.switchUser(read);
await t.throwsAsync( await t.throwsAsync(
inviteUsers(app, read.token.token, ws.id, ['test@affine.pro']), inviteUsers(app, ws.id, ['test@affine.pro']),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not manager' 'should throw error if not manager'
); );
app.switchUser(write);
await t.throwsAsync( await t.throwsAsync(
inviteUsers(app, write.token.token, ws.id, ['test@affine.pro']), inviteUsers(app, ws.id, ['test@affine.pro']),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not manager' 'should throw error if not manager'
); );
@@ -244,20 +223,22 @@ test('should be able to invite multiple users', async t => {
{ {
// manager // manager
const m1 = await signUp(app, 'm1', 'm1@affine.pro', '123456'); const m1 = await app.signup('m1@affine.pro');
const m2 = await signUp(app, 'm2', 'm2@affine.pro', '123456'); const m2 = await app.signup('m2@affine.pro');
app.switchUser(owner);
t.is( t.is(
(await inviteUsers(app, owner.token.token, ws.id, [m1.email])).length, (await inviteUsers(app, ws.id, [m1.email])).length,
1,
'should be able to invite user'
);
app.switchUser(admin);
t.is(
(await inviteUsers(app, ws.id, [m2.email])).length,
1, 1,
'should be able to invite user' 'should be able to invite user'
); );
t.is( t.is(
(await inviteUsers(app, admin.token.token, ws.id, [m2.email])).length, (await inviteUsers(app, ws.id, [m2.email])).length,
1,
'should be able to invite user'
);
t.is(
(await inviteUsers(app, admin.token.token, ws.id, [m2.email])).length,
0, 0,
'should not be able to invite user if already in workspace' 'should not be able to invite user if already in workspace'
); );
@@ -265,7 +246,6 @@ test('should be able to invite multiple users', async t => {
await t.throwsAsync( await t.throwsAsync(
inviteUsers( inviteUsers(
app, app,
admin.token.token,
ws.id, ws.id,
Array.from({ length: 513 }, (_, i) => `m${i}@affine.pro`) Array.from({ length: 513 }, (_, i) => `m${i}@affine.pro`)
), ),
@@ -313,7 +293,7 @@ test('should be able to check seat limit', async t => {
); );
// refresh seat, fifo // refresh seat, fifo
sleep(1000); await sleep(1000);
const [[members2]] = await inviteBatch(['member6@affine.pro']); const [[members2]] = await inviteBatch(['member6@affine.pro']);
await permissions.refreshSeatStatus(ws.id, 7); await permissions.refreshSeatStatus(ws.id, 7);
@@ -337,42 +317,23 @@ test('should be able to grant team member permission', async t => {
const { app, permissions } = t.context; const { app, permissions } = t.context;
const { owner, teamWorkspace: ws, write, read } = await init(app); const { owner, teamWorkspace: ws, write, read } = await init(app);
app.switchUser(read);
await t.throwsAsync( await t.throwsAsync(
grantMember( grantMember(app, ws.id, write.id, WorkspaceRole.Collaborator),
app,
read.token.token,
ws.id,
write.id,
WorkspaceRole.Collaborator
),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not owner' 'should throw error if not owner'
); );
app.switchUser(write);
await t.throwsAsync( await t.throwsAsync(
grantMember( grantMember(app, ws.id, read.id, WorkspaceRole.Collaborator),
app,
write.token.token,
ws.id,
read.id,
WorkspaceRole.Collaborator
),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not owner' 'should throw error if not owner'
); );
await t.throwsAsync(
grantMember(
app,
write.token.token,
ws.id,
read.id,
WorkspaceRole.Collaborator
),
{ instanceOf: Error },
'should throw error if not admin'
);
{ {
// owner should be able to grant permission // owner should be able to grant permission
app.switchUser(owner);
t.true( t.true(
await permissions.tryCheckWorkspaceIs( await permissions.tryCheckWorkspaceIs(
ws.id, ws.id,
@@ -382,13 +343,7 @@ test('should be able to grant team member permission', async t => {
'should be able to check permission' 'should be able to check permission'
); );
t.truthy( t.truthy(
await grantMember( await grantMember(app, ws.id, read.id, WorkspaceRole.Admin),
app,
owner.token.token,
ws.id,
read.id,
WorkspaceRole.Admin
),
'should be able to grant permission' 'should be able to grant permission'
); );
t.true( t.true(
@@ -406,20 +361,27 @@ test('should be able to leave workspace', async t => {
const { app } = t.context; const { app } = t.context;
const { owner, teamWorkspace: ws, admin, write, read } = await init(app); const { owner, teamWorkspace: ws, admin, write, read } = await init(app);
app.switchUser(owner);
t.false( t.false(
await leaveWorkspace(app, owner.token.token, ws.id), await leaveWorkspace(app, ws.id),
'owner should not be able to leave workspace' 'owner should not be able to leave workspace'
); );
app.switchUser(admin);
t.true( t.true(
await leaveWorkspace(app, admin.token.token, ws.id), await leaveWorkspace(app, ws.id),
'admin should be able to leave workspace' 'admin should be able to leave workspace'
); );
app.switchUser(write);
t.true( t.true(
await leaveWorkspace(app, write.token.token, ws.id), await leaveWorkspace(app, ws.id),
'write should be able to leave workspace' 'write should be able to leave workspace'
); );
app.switchUser(read);
t.true( t.true(
await leaveWorkspace(app, read.token.token, ws.id), await leaveWorkspace(app, ws.id),
'read should be able to leave workspace' 'read should be able to leave workspace'
); );
}); });
@@ -430,13 +392,14 @@ test('should be able to revoke team member', async t => {
{ {
// no permission // no permission
app.switchUser(read);
await t.throwsAsync( await t.throwsAsync(
revokeUser(app, read.token.token, ws.id, read.id), revokeUser(app, ws.id, read.id),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not admin' 'should throw error if not admin'
); );
await t.throwsAsync( await t.throwsAsync(
revokeUser(app, read.token.token, ws.id, write.id), revokeUser(app, ws.id, write.id),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not admin' 'should throw error if not admin'
); );
@@ -444,32 +407,35 @@ test('should be able to revoke team member', async t => {
{ {
// manager // manager
app.switchUser(admin);
t.true( t.true(
await revokeUser(app, admin.token.token, ws.id, read.id), await revokeUser(app, ws.id, read.id),
'admin should be able to revoke member' 'admin should be able to revoke member'
); );
await t.throwsAsync(
revokeUser(app, ws.id, admin.id),
{ instanceOf: Error },
'should not be able to revoke themselves'
);
app.switchUser(owner);
t.true( t.true(
await revokeUser(app, owner.token.token, ws.id, write.id), await revokeUser(app, ws.id, write.id),
'owner should be able to revoke member' 'owner should be able to revoke member'
); );
await t.throwsAsync(
revokeUser(app, admin.token.token, ws.id, admin.id),
{ instanceOf: Error },
'should not be able to revoke themselves'
);
t.false( t.false(
await revokeUser(app, owner.token.token, ws.id, owner.id), await revokeUser(app, ws.id, owner.id),
'should not be able to revoke themselves' 'should not be able to revoke themselves'
); );
await revokeUser(app, owner.token.token, ws.id, admin.id); await revokeUser(app, ws.id, admin.id);
app.switchUser(admin);
await t.throwsAsync( await t.throwsAsync(
revokeUser(app, admin.token.token, ws.id, read.id), revokeUser(app, ws.id, read.id),
{ instanceOf: Error }, { instanceOf: Error },
'should not be able to revoke member not in workspace' 'should not be able to revoke member not in workspace after revoked'
); );
} }
}); });
@@ -490,38 +456,31 @@ test('should be able to manage invite link', async t => {
[tws, [owner, admin]], [tws, [owner, admin]],
] as const) { ] as const) {
for (const manager of managers) { for (const manager of managers) {
const { link } = await createInviteLink( app.switchUser(manager.id);
app, const { link } = await createInviteLink(app, workspace.id, 'OneDay');
manager.token.token, const { link: currLink } = await getInviteLink(app, workspace.id);
workspace.id,
'OneDay'
);
const { link: currLink } = await getInviteLink(
app,
manager.token.token,
workspace.id
);
t.is(link, currLink, 'should be able to get invite link'); t.is(link, currLink, 'should be able to get invite link');
t.true( t.true(
await revokeInviteLink(app, manager.token.token, workspace.id), await revokeInviteLink(app, workspace.id),
'should be able to revoke invite link' 'should be able to revoke invite link'
); );
} }
for (const collaborator of [write, read]) { for (const collaborator of [write, read]) {
app.switchUser(collaborator.id);
await t.throwsAsync( await t.throwsAsync(
createInviteLink(app, collaborator.token.token, workspace.id, 'OneDay'), createInviteLink(app, workspace.id, 'OneDay'),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not manager' 'should throw error if not manager'
); );
await t.throwsAsync( await t.throwsAsync(
getInviteLink(app, collaborator.token.token, workspace.id), getInviteLink(app, workspace.id),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not manager' 'should throw error if not manager'
); );
await t.throwsAsync( await t.throwsAsync(
revokeInviteLink(app, collaborator.token.token, workspace.id), revokeInviteLink(app, workspace.id),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not manager' 'should throw error if not manager'
); );
@@ -534,48 +493,42 @@ test('should be able to approve team member', async t => {
const { teamWorkspace: tws, owner, admin, write, read } = await init(app, 6); const { teamWorkspace: tws, owner, admin, write, read } = await init(app, 6);
{ {
const { link } = await createInviteLink( app.switchUser(owner);
app, const { link } = await createInviteLink(app, tws.id, 'OneDay');
owner.token.token,
tws.id,
'OneDay'
);
const inviteId = link.split('/').pop()!; const inviteId = link.split('/').pop()!;
const member = await signUp( const member = await app.signup('newmember@affine.pro');
app,
'newmember',
'newmember@affine.pro',
'123456'
);
t.true( t.true(
await acceptInviteById(app, tws.id, inviteId, false, member.token.token), await acceptInviteById(app, tws.id, inviteId, false),
'should be able to accept invite' 'should be able to accept invite'
); );
const { members } = await getWorkspace(app, owner.token.token, tws.id); app.switchUser(owner);
const { members } = await getWorkspace(app, tws.id);
const memberInvite = members.find(m => m.id === member.id)!; const memberInvite = members.find(m => m.id === member.id)!;
t.is(memberInvite.status, 'UnderReview', 'should be under review'); t.is(memberInvite.status, 'UnderReview', 'should be under review');
t.is( t.is(await approveMember(app, tws.id, member.id), memberInvite.inviteId);
await approveMember(app, admin.token.token, tws.id, member.id),
memberInvite.inviteId
);
} }
{ {
app.switchUser(admin);
await t.throwsAsync( await t.throwsAsync(
approveMember(app, admin.token.token, tws.id, 'not_exists_id'), approveMember(app, tws.id, 'not_exists_id'),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if member not exists' 'should throw error if member not exists'
); );
app.switchUser(write);
await t.throwsAsync( await t.throwsAsync(
approveMember(app, write.token.token, tws.id, 'not_exists_id'), approveMember(app, tws.id, 'not_exists_id'),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not manager' 'should throw error if not manager'
); );
app.switchUser(read);
await t.throwsAsync( await t.throwsAsync(
approveMember(app, read.token.token, tws.id, 'not_exists_id'), approveMember(app, tws.id, 'not_exists_id'),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not manager' 'should throw error if not manager'
); );
@@ -596,11 +549,12 @@ test('should be able to invite by link', async t => {
{ {
// check invite link // check invite link
const info = await getInviteInfo(app, owner.token.token, inviteId); app.switchUser(owner);
const info = await getInviteInfo(app, inviteId);
t.is(info.workspace.id, ws.id, 'should be able to get invite info'); t.is(info.workspace.id, ws.id, 'should be able to get invite info');
// check team invite link // check team invite link
const teamInfo = await getInviteInfo(app, owner.token.token, teamInviteId); const teamInfo = await getInviteInfo(app, teamInviteId);
t.is(teamInfo.workspace.id, tws.id, 'should be able to get invite info'); t.is(teamInfo.workspace.id, tws.id, 'should be able to get invite info');
} }
@@ -625,7 +579,7 @@ test('should be able to invite by link', async t => {
{ {
// team invite link // team invite link
const members: UserAuthedType[] = []; const members: User[] = [];
await t.notThrowsAsync(async () => { await t.notThrowsAsync(async () => {
members.push(await teamInvite('member3@affine.pro')); members.push(await teamInvite('member3@affine.pro'));
members.push(await teamInvite('member4@affine.pro')); members.push(await teamInvite('member4@affine.pro'));
@@ -669,12 +623,9 @@ test('should be able to invite by link', async t => {
); );
{ {
const message = `You have already joined in Space ${tws.id}.`; await t.throwsAsync(acceptTeamInvite(owner.id), {
await t.throwsAsync( message: `You have already joined in Space ${tws.id}.`,
acceptTeamInvite(owner.token.token), });
{ message },
'should throw error if member already in workspace'
);
} }
} }
}); });
@@ -714,7 +665,7 @@ test('should be able to emit events', async t => {
const { teamWorkspace: tws, owner, createInviteLink } = await init(app); const { teamWorkspace: tws, owner, createInviteLink } = await init(app);
const [, invite] = await createInviteLink(tws); const [, invite] = await createInviteLink(tws);
const user = await invite('m3@affine.pro'); const user = await invite('m3@affine.pro');
const { members } = await getWorkspace(app, owner.token.token, tws.id); const { members } = await getWorkspace(app, tws.id);
const memberInvite = members.find(m => m.id === user.id)!; const memberInvite = members.find(m => m.id === user.id)!;
t.deepEqual( t.deepEqual(
event.emit.lastCall.args, event.emit.lastCall.args,
@@ -725,7 +676,8 @@ test('should be able to emit events', async t => {
'should emit review requested event' 'should emit review requested event'
); );
await revokeUser(app, owner.token.token, tws.id, user.id); app.switchUser(owner);
await revokeUser(app, tws.id, user.id);
t.deepEqual( t.deepEqual(
event.emit.lastCall.args, event.emit.lastCall.args,
[ [
@@ -738,13 +690,7 @@ test('should be able to emit events', async t => {
{ {
const { teamWorkspace: tws, owner, read } = await init(app); const { teamWorkspace: tws, owner, read } = await init(app);
await grantMember( await grantMember(app, tws.id, read.id, WorkspaceRole.Admin);
app,
owner.token.token,
tws.id,
read.id,
WorkspaceRole.Admin
);
t.deepEqual( t.deepEqual(
event.emit.lastCall.args, event.emit.lastCall.args,
[ [
@@ -758,13 +704,7 @@ test('should be able to emit events', async t => {
'should emit role changed event' 'should emit role changed event'
); );
await grantMember( await grantMember(app, tws.id, read.id, WorkspaceRole.Owner);
app,
owner.token.token,
tws.id,
read.id,
WorkspaceRole.Owner
);
const [ownershipTransferred] = event.emit const [ownershipTransferred] = event.emit
.getCalls() .getCalls()
.map(call => call.args) .map(call => call.args)
@@ -778,7 +718,8 @@ test('should be able to emit events', async t => {
'should emit owner transferred event' 'should emit owner transferred event'
); );
await revokeMember(app, read.token.token, tws.id, owner.id); app.switchUser(read);
await revokeMember(app, tws.id, owner.id);
const [memberRemoved, memberUpdated] = event.emit const [memberRemoved, memberUpdated] = event.emit
.getCalls() .getCalls()
.map(call => call.args) .map(call => call.args)
@@ -819,55 +760,40 @@ test('should be able to grant and revoke users role in page', async t => {
} = await init(app, 5); } = await init(app, 5);
const docId = nanoid(); const docId = nanoid();
app.switchUser(admin);
const res = await grantDocUserRoles( const res = await grantDocUserRoles(
app, app,
admin.token.token,
ws.id, ws.id,
docId, docId,
[read.id, write.id], [read.id, write.id],
DocRole.Manager DocRole.Manager
); );
t.deepEqual(res.body, { t.deepEqual(res, {
data: {
grantDocUserRoles: true, grantDocUserRoles: true,
},
}); });
// should not downgrade the role if role exists // should not downgrade the role if role exists
{ {
await grantDocUserRoles( await grantDocUserRoles(app, ws.id, docId, [read.id], DocRole.Reader);
app,
admin.token.token,
ws.id,
docId,
[read.id],
DocRole.Reader
);
// read still be the Manager of this doc // read still be the Manager of this doc
app.switchUser(read);
const res = await grantDocUserRoles( const res = await grantDocUserRoles(
app, app,
read.token.token,
ws.id, ws.id,
docId, docId,
[external.id], [external.id],
DocRole.Editor DocRole.Editor
); );
t.deepEqual(res.body, { t.deepEqual(res, {
data: {
grantDocUserRoles: true, grantDocUserRoles: true,
},
}); });
const docUsersList = await docGrantedUsersList( app.switchUser(admin);
app, const docUsersList = await docGrantedUsersList(app, ws.id, docId);
admin.token.token, t.is(docUsersList.workspace.doc.grantedUsersList.totalCount, 3);
ws.id, const externalRole = docUsersList.workspace.doc.grantedUsersList.edges.find(
docId
);
t.is(docUsersList.data.workspace.doc.grantedUsersList.totalCount, 3);
const externalRole =
docUsersList.data.workspace.doc.grantedUsersList.edges.find(
(edge: any) => edge.node.user.id === external.id (edge: any) => edge.node.user.id === external.id
)?.node.role; )?.node.role;
t.is(externalRole, DocRole[DocRole.Editor]); t.is(externalRole, DocRole[DocRole.Editor]);
@@ -878,18 +804,11 @@ test('should be able to change the default role in page', async t => {
const { app } = t.context; const { app } = t.context;
const { teamWorkspace: ws, admin } = await init(app, 5); const { teamWorkspace: ws, admin } = await init(app, 5);
const docId = nanoid(); const docId = nanoid();
const res = await updateDocDefaultRole( app.switchUser(admin);
app, const res = await updateDocDefaultRole(app, ws.id, docId, DocRole.Reader);
admin.token.token,
ws.id,
docId,
DocRole.Reader
);
t.deepEqual(res.body, { t.deepEqual(res, {
data: {
updateDocDefaultRole: true, updateDocDefaultRole: true,
},
}); });
}); });
@@ -904,54 +823,42 @@ test('default page role should be able to override the workspace role', async t
const docId = nanoid(); const docId = nanoid();
app.switchUser(admin);
const res = await updateDocDefaultRole( const res = await updateDocDefaultRole(
app, app,
admin.token.token,
workspace.id, workspace.id,
docId, docId,
DocRole.Manager DocRole.Manager
); );
t.deepEqual(res.body, { t.deepEqual(res, {
data: {
updateDocDefaultRole: true, updateDocDefaultRole: true,
},
}); });
// reader can manage the page if the page default role is Manager // reader can manage the page if the page default role is Manager
{ {
app.switchUser(read);
const readerRes = await updateDocDefaultRole( const readerRes = await updateDocDefaultRole(
app, app,
read.token.token,
workspace.id, workspace.id,
docId, docId,
DocRole.Manager DocRole.Manager
); );
t.deepEqual(readerRes.body, { t.deepEqual(readerRes, {
data: {
updateDocDefaultRole: true, updateDocDefaultRole: true,
},
}); });
} }
// external can't manage the page even if the page default role is Manager // external can't manage the page even if the page default role is Manager
{ {
const externalRes = await updateDocDefaultRole( app.switchUser(external);
app, await t.throwsAsync(
external.token.token, updateDocDefaultRole(app, workspace.id, docId, DocRole.Manager),
workspace.id,
docId,
DocRole.Manager
);
t.like(externalRes.body, {
errors: [
{ {
message: `You do not have permission to access doc ${docId} under Space ${workspace.id}.`, message: `You do not have permission to access doc ${docId} under Space ${workspace.id}.`,
}, }
], );
});
} }
}); });
@@ -960,70 +867,48 @@ test('should be able to grant and revoke doc user role', async t => {
const { teamWorkspace: ws, admin, read, external } = await init(app, 5); const { teamWorkspace: ws, admin, read, external } = await init(app, 5);
const docId = nanoid(); const docId = nanoid();
app.switchUser(admin);
const res = await grantDocUserRoles( const res = await grantDocUserRoles(
app, app,
admin.token.token,
ws.id, ws.id,
docId, docId,
[external.id], [external.id],
DocRole.Manager DocRole.Manager
); );
t.deepEqual(res.body, { t.deepEqual(res, {
data: {
grantDocUserRoles: true, grantDocUserRoles: true,
},
}); });
// external user now can manage the page // external user now can manage the page
{ {
app.switchUser(external);
const externalRes = await grantDocUserRoles( const externalRes = await grantDocUserRoles(
app, app,
external.token.token,
ws.id, ws.id,
docId, docId,
[read.id], [read.id],
DocRole.Manager DocRole.Manager
); );
t.deepEqual(externalRes.body, { t.deepEqual(externalRes, {
data: {
grantDocUserRoles: true, grantDocUserRoles: true,
},
}); });
} }
// revoke the role of the external user // revoke the role of the external user
{ {
const revokeRes = await revokeDocUserRoles( app.switchUser(admin);
app, const revokeRes = await revokeDocUserRoles(app, ws.id, docId, external.id);
admin.token.token,
ws.id,
docId,
external.id
);
t.deepEqual(revokeRes.body, { t.deepEqual(revokeRes, {
data: {
revokeDocUserRoles: true, revokeDocUserRoles: true,
},
}); });
// external user can't manage the page // external user can't manage the page
const externalRes = await revokeDocUserRoles( app.switchUser(external);
app, await t.throwsAsync(revokeDocUserRoles(app, ws.id, docId, read.id), {
external.token.token,
ws.id,
docId,
read.id
);
t.like(externalRes.body, {
errors: [
{
message: `You do not have permission to access doc ${docId} under Space ${ws.id}.`, message: `You do not have permission to access doc ${docId} under Space ${ws.id}.`,
},
],
}); });
} }
}); });
@@ -1033,27 +918,11 @@ test('update page default role should throw error if the space does not exist',
const { admin } = await init(app, 5); const { admin } = await init(app, 5);
const docId = nanoid(); const docId = nanoid();
const nonExistWorkspaceId = 'non-exist-workspace'; const nonExistWorkspaceId = 'non-exist-workspace';
const res = await request(app.getHttpServer()) app.switchUser(admin);
.post('/graphql') await t.throwsAsync(
.auth(admin.token.token, { type: 'bearer' }) updateDocDefaultRole(app, nonExistWorkspaceId, docId, DocRole.Manager),
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
updateDocDefaultRole(input: {
workspaceId: "${nonExistWorkspaceId}",
docId: "${docId}",
role: Manager,
})
}
`,
})
.expect(200);
t.like(res.body, {
errors: [
{ {
message: `You do not have permission to access doc ${docId} under Space ${nonExistWorkspaceId}.`, message: `You do not have permission to access doc ${docId} under Space ${nonExistWorkspaceId}.`,
}, }
], );
});
}); });

View File

@@ -1,16 +1,16 @@
import test from 'ava'; import test from 'ava';
import request from 'supertest';
import { AppModule } from '../app.module'; import {
import { createTestingApp, currentUser, signUp, TestingApp } from './utils'; createTestingApp,
currentUser,
deleteAccount,
TestingApp,
} from './utils';
let app: TestingApp; let app: TestingApp;
test.before(async () => { test.before(async () => {
const { app: testApp } = await createTestingApp({ app = await createTestingApp();
imports: [AppModule],
});
app = testApp;
}); });
test.beforeEach(async () => { test.beforeEach(async () => {
@@ -21,16 +21,12 @@ test.after.always(async () => {
await app.close(); await app.close();
}); });
test('should register a user', async t => { // TODO(@forehalo): signup test case
const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); test.skip('should register a user', () => {});
t.is(typeof user.id, 'string', 'user.id is not a string');
t.is(user.name, 'u1', 'user.name is not valid');
t.is(user.email, 'u1@affine.pro', 'user.email is not valid');
});
test('should get current user', async t => { test('should get current user', async t => {
const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); const user = await app.signup('u1@affine.pro');
const currUser = await currentUser(app, user.token.token); const currUser = await currentUser(app);
t.is(currUser.id, user.id, 'user.id is not valid'); t.is(currUser.id, user.id, 'user.id is not valid');
t.is(currUser.name, user.name, 'user.name is not valid'); t.is(currUser.name, user.name, 'user.name is not valid');
t.is(currUser.email, user.email, 'user.email is not valid'); t.is(currUser.email, user.email, 'user.email is not valid');
@@ -38,20 +34,9 @@ test('should get current user', async t => {
}); });
test('should be able to delete user', async t => { test('should be able to delete user', async t => {
const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); await app.signup('u1@affine.pro');
await request(app.getHttpServer()) const deleted = await deleteAccount(app);
.post('/graphql') t.true(deleted);
.auth(user.token.token, { type: 'bearer' }) const currUser = await currentUser(app);
.send({ t.is(currUser, null);
query: `
mutation {
deleteAccount {
success
}
}
`,
})
.expect(200);
t.is(await currentUser(app, user.token.token), null);
t.pass();
}); });

View File

@@ -1,71 +1,37 @@
import type { INestApplication } from '@nestjs/common';
import type { TestFn } from 'ava'; import type { TestFn } from 'ava';
import ava from 'ava'; import ava from 'ava';
import request from 'supertest';
import { AppModule } from '../../app.module'; import { createTestingApp, TestingApp, updateAvatar } from '../utils';
import { AuthService, CurrentUser } from '../../core/auth';
import { createTestingApp, gql, internalSignIn } from '../utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
app: INestApplication; app: TestingApp;
u1: CurrentUser;
}>; }>;
test.beforeEach(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp();
imports: [AppModule],
});
t.context.u1 = await app.get(AuthService).signUp('u1@affine.pro', '1');
t.context.app = app; t.context.app = app;
}); });
test.afterEach.always(async t => { test.beforeEach(async t => {
await t.context.app.close(); await t.context.app.initTestingDB();
}); });
async function fakeUploadAvatar( test.after.always(async t => {
app: INestApplication, await t.context.app.close();
userId: string, });
avatar: Buffer
) {
const cookie = await internalSignIn(app, userId);
return gql(app)
.set('Cookie', cookie)
.field(
'operations',
JSON.stringify({
name: 'uploadAvatar',
query: `mutation uploadAvatar($avatar: Upload!) {
uploadAvatar(avatar: $avatar) {
avatarUrl
}
}`,
variables: { avatar: null },
})
)
.field('map', JSON.stringify({ '0': ['variables.avatar'] }))
.attach('0', avatar, {
filename: 'test.png',
contentType: 'image/png',
});
}
test('should be able to upload user avatar', async t => { test('should be able to upload user avatar', async t => {
const { app } = t.context; const { app } = t.context;
await app.signup('u1@affine.pro');
const avatar = Buffer.from('test'); const avatar = Buffer.from('test');
const res = await fakeUploadAvatar(app, t.context.u1.id, avatar); const res = await updateAvatar(app, avatar);
t.is(res.status, 200); t.is(res.status, 200);
const avatarUrl = res.body.data.uploadAvatar.avatarUrl; const avatarUrl = res.body.data.uploadAvatar.avatarUrl;
t.truthy(avatarUrl); t.truthy(avatarUrl);
const avatarRes = await request(app.getHttpServer()) const avatarRes = await app.GET(new URL(avatarUrl).pathname);
.get(new URL(avatarUrl).pathname)
.expect(200);
t.deepEqual(avatarRes.body, Buffer.from('test')); t.deepEqual(avatarRes.body, Buffer.from('test'));
}); });
@@ -73,24 +39,21 @@ test('should be able to upload user avatar', async t => {
test('should be able to update user avatar, and invalidate old avatar url', async t => { test('should be able to update user avatar, and invalidate old avatar url', async t => {
const { app } = t.context; const { app } = t.context;
await app.signup('u1@affine.pro');
const avatar = Buffer.from('test'); const avatar = Buffer.from('test');
let res = await fakeUploadAvatar(app, t.context.u1.id, avatar); let res = await updateAvatar(app, avatar);
const oldAvatarUrl = res.body.data.uploadAvatar.avatarUrl; const oldAvatarUrl = res.body.data.uploadAvatar.avatarUrl;
const newAvatar = Buffer.from('new'); const newAvatar = Buffer.from('new');
res = await fakeUploadAvatar(app, t.context.u1.id, newAvatar); res = await updateAvatar(app, newAvatar);
const newAvatarUrl = res.body.data.uploadAvatar.avatarUrl; const newAvatarUrl = res.body.data.uploadAvatar.avatarUrl;
t.not(oldAvatarUrl, newAvatarUrl); t.not(oldAvatarUrl, newAvatarUrl);
await request(app.getHttpServer()) const avatarRes = await app.GET(new URL(oldAvatarUrl).pathname);
.get(new URL(oldAvatarUrl).pathname) t.is(avatarRes.status, 404);
.expect(404);
const avatarRes = await request(app.getHttpServer()) const newAvatarRes = await app.GET(new URL(newAvatarUrl).pathname);
.get(new URL(newAvatarUrl).pathname) t.deepEqual(newAvatarRes.body, Buffer.from('new'));
.expect(200);
t.deepEqual(avatarRes.body, Buffer.from('new'));
}); });

View File

@@ -1,58 +1,33 @@
import type { INestApplication } from '@nestjs/common'; import { TestingApp } from './testing-app';
import request from 'supertest';
import { gql } from './common';
export async function listBlobs( export async function listBlobs(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string workspaceId: string
): Promise<string[]> { ): Promise<string[]> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
query { query {
listBlobs(workspaceId: "${workspaceId}") listBlobs(workspaceId: "${workspaceId}")
} }
`, `);
}) return res.listBlobs;
.expect(200);
return res.body.data.listBlobs;
} }
export async function getWorkspaceBlobsSize( export async function getWorkspaceBlobsSize(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string workspaceId: string
): Promise<number> { ): Promise<number> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.send({
query: `
query { query {
workspace(id: "${workspaceId}") { workspace(id: "${workspaceId}") {
blobsSize blobsSize
} }
} }
`, `);
}) return res.workspace.blobsSize;
.expect(200);
return res.body.data.workspace.blobsSize;
} }
export async function collectAllBlobSizes( export async function collectAllBlobSizes(app: TestingApp): Promise<number> {
app: INestApplication, const res = await app.gql(`
token: string
): Promise<number> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.send({
query: `
query { query {
currentUser { currentUser {
quotaUsage { quotaUsage {
@@ -60,21 +35,17 @@ export async function collectAllBlobSizes(
} }
} }
} }
`, `);
}) return res.currentUser.quotaUsage.storageQuota;
.expect(200);
return res.body.data.currentUser.quotaUsage.storageQuota;
} }
export async function setBlob( export async function setBlob(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
buffer: Buffer buffer: Buffer
): Promise<string> { ): Promise<string> {
const res = await request(app.getHttpServer()) const res = await app
.post(gql) .POST('/graphql')
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.field( .field(
'operations', 'operations',

View File

@@ -1,8 +1,5 @@
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { import {
DEFAULT_DIMENSIONS, DEFAULT_DIMENSIONS,
OpenAIProvider, OpenAIProvider,
@@ -26,8 +23,8 @@ import {
WorkflowNodeType, WorkflowNodeType,
WorkflowParams, WorkflowParams,
} from '../../plugins/copilot/workflow/types'; } from '../../plugins/copilot/workflow/types';
import { gql } from './common'; import { TestingApp } from './testing-app';
import { handleGraphQLError, sleep } from './utils'; import { sleep } from './utils';
// @ts-expect-error no error // @ts-expect-error no error
export class MockCopilotTestProvider export class MockCopilotTestProvider
@@ -159,167 +156,123 @@ export class MockCopilotTestProvider
} }
export async function createCopilotSession( export async function createCopilotSession(
app: INestApplication, app: TestingApp,
userToken: string,
workspaceId: string, workspaceId: string,
docId: string, docId: string,
promptName: string promptName: string
): Promise<string> { ): Promise<string> {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation createCopilotSession($options: CreateChatSessionInput!) { mutation createCopilotSession($options: CreateChatSessionInput!) {
createCopilotSession(options: $options) createCopilotSession(options: $options)
} }
`, `,
variables: { options: { workspaceId, docId, promptName } }, { options: { workspaceId, docId, promptName } }
}) );
.expect(200);
handleGraphQLError(res); return res.createCopilotSession;
return res.body.data.createCopilotSession;
} }
export async function updateCopilotSession( export async function updateCopilotSession(
app: INestApplication, app: TestingApp,
userToken: string,
sessionId: string, sessionId: string,
promptName: string promptName: string
): Promise<string> { ): Promise<string> {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation updateCopilotSession($options: UpdateChatSessionInput!) { mutation updateCopilotSession($options: UpdateChatSessionInput!) {
updateCopilotSession(options: $options) updateCopilotSession(options: $options)
} }
`, `,
variables: { options: { sessionId, promptName } }, { options: { sessionId, promptName } }
}) );
.expect(200);
handleGraphQLError(res); return res.updateCopilotSession;
return res.body.data.updateCopilotSession;
} }
export async function forkCopilotSession( export async function forkCopilotSession(
app: INestApplication, app: TestingApp,
userToken: string,
workspaceId: string, workspaceId: string,
docId: string, docId: string,
sessionId: string, sessionId: string,
latestMessageId: string latestMessageId: string
): Promise<string> { ): Promise<string> {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation forkCopilotSession($options: ForkChatSessionInput!) { mutation forkCopilotSession($options: ForkChatSessionInput!) {
forkCopilotSession(options: $options) forkCopilotSession(options: $options)
} }
`, `,
variables: { { options: { workspaceId, docId, sessionId, latestMessageId } }
options: { workspaceId, docId, sessionId, latestMessageId }, );
},
})
.expect(200);
handleGraphQLError(res); return res.forkCopilotSession;
return res.body.data.forkCopilotSession;
} }
export async function createCopilotMessage( export async function createCopilotMessage(
app: INestApplication, app: TestingApp,
userToken: string,
sessionId: string, sessionId: string,
content?: string, content?: string,
attachments?: string[], attachments?: string[],
blobs?: ArrayBuffer[], blobs?: ArrayBuffer[],
params?: Record<string, string> params?: Record<string, string>
): Promise<string> { ): Promise<string> {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation createCopilotMessage($options: CreateChatMessageInput!) { mutation createCopilotMessage($options: CreateChatMessageInput!) {
createCopilotMessage(options: $options) createCopilotMessage(options: $options)
} }
`, `,
variables: { { options: { sessionId, content, attachments, blobs, params } }
options: { sessionId, content, attachments, blobs, params }, );
},
})
.expect(200);
handleGraphQLError(res); return res.createCopilotMessage;
return res.body.data.createCopilotMessage;
} }
export async function chatWithText( export async function chatWithText(
app: INestApplication, app: TestingApp,
userToken: string,
sessionId: string, sessionId: string,
messageId?: string, messageId?: string,
prefix = '' prefix = ''
): Promise<string> { ): Promise<string> {
const query = messageId ? `?messageId=${messageId}` : ''; const query = messageId ? `?messageId=${messageId}` : '';
const res = await request(app.getHttpServer()) const res = await app
.get(`/api/copilot/chat/${sessionId}${prefix}${query}`) .GET(`/api/copilot/chat/${sessionId}${prefix}${query}`)
.auth(userToken, { type: 'bearer' })
.expect(200); .expect(200);
return res.text; return res.text;
} }
export async function chatWithTextStream( export async function chatWithTextStream(
app: INestApplication, app: TestingApp,
userToken: string,
sessionId: string, sessionId: string,
messageId?: string messageId?: string
) { ) {
return chatWithText(app, userToken, sessionId, messageId, '/stream'); return chatWithText(app, sessionId, messageId, '/stream');
} }
export async function chatWithWorkflow( export async function chatWithWorkflow(
app: INestApplication, app: TestingApp,
userToken: string,
sessionId: string, sessionId: string,
messageId?: string messageId?: string
) { ) {
return chatWithText(app, userToken, sessionId, messageId, '/workflow'); return chatWithText(app, sessionId, messageId, '/workflow');
} }
export async function chatWithImages( export async function chatWithImages(
app: INestApplication, app: TestingApp,
userToken: string,
sessionId: string, sessionId: string,
messageId?: string messageId?: string
) { ) {
return chatWithText(app, userToken, sessionId, messageId, '/images'); return chatWithText(app, sessionId, messageId, '/images');
} }
export async function unsplashSearch( export async function unsplashSearch(
app: INestApplication, app: TestingApp,
userToken: string,
params: Record<string, string> = {} params: Record<string, string> = {}
) { ) {
const query = new URLSearchParams(params); const query = new URLSearchParams(params);
const res = await request(app.getHttpServer()) const res = await app.GET(`/api/copilot/unsplash/photos?${query}`);
.get(`/api/copilot/unsplash/photos?${query}`)
.auth(userToken, { type: 'bearer' });
return res; return res;
} }
@@ -378,8 +331,7 @@ type History = {
}; };
export async function getHistories( export async function getHistories(
app: INestApplication, app: TestingApp,
userToken: string,
variables: { variables: {
workspaceId: string; workspaceId: string;
docId?: string; docId?: string;
@@ -394,12 +346,8 @@ export async function getHistories(
}; };
} }
): Promise<History[]> { ): Promise<History[]> {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
query getCopilotHistories( query getCopilotHistories(
$workspaceId: String! $workspaceId: String!
$docId: String $docId: String
@@ -424,13 +372,10 @@ export async function getHistories(
} }
} }
`, `,
variables, variables
}) );
.expect(200);
handleGraphQLError(res); return res.currentUser?.copilot?.histories || [];
return res.body.data.currentUser?.copilot?.histories || [];
} }
type Prompt = { type Prompt = {

View File

@@ -1,6 +1,8 @@
export * from './blobs'; export * from './blobs';
export * from './invite'; export * from './invite';
export * from './permission'; export * from './permission';
export * from './testing-app';
export * from './testing-module';
export * from './user'; export * from './user';
export * from './utils'; export * from './utils';
export * from './workspace'; export * from './workspace';

View File

@@ -1,47 +1,28 @@
import type { INestApplication } from '@nestjs/common';
import request from 'supertest';
import type { InvitationType } from '../../core/workspaces'; import type { InvitationType } from '../../core/workspaces';
import { gql } from './common'; import type { TestingApp } from './testing-app';
export async function inviteUser( export async function inviteUser(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
email: string, email: string,
sendInviteMail = false sendInviteMail = false
): Promise<string> { ): Promise<string> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
invite(workspaceId: "${workspaceId}", email: "${email}", sendInviteMail: ${sendInviteMail}) invite(workspaceId: "${workspaceId}", email: "${email}", sendInviteMail: ${sendInviteMail})
} }
`, `);
})
.expect(200); return res.invite;
if (res.body.errors) {
throw new Error(res.body.errors[0].message);
}
return res.body.data.invite;
} }
export async function inviteUsers( export async function inviteUsers(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
emails: string[], emails: string[],
sendInviteMail = false sendInviteMail = false
): Promise<Array<{ email: string; inviteId?: string; sentSuccess?: boolean }>> { ): Promise<Array<{ email: string; inviteId?: string; sentSuccess?: boolean }>> {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation inviteBatch($workspaceId: String!, $emails: [String!]!, $sendInviteMail: Boolean) { mutation inviteBatch($workspaceId: String!, $emails: [String!]!, $sendInviteMail: Boolean) {
inviteBatch( inviteBatch(
workspaceId: $workspaceId workspaceId: $workspaceId
@@ -54,26 +35,17 @@ export async function inviteUsers(
} }
} }
`, `,
variables: { workspaceId, emails, sendInviteMail }, { workspaceId, emails, sendInviteMail }
}) );
.expect(200);
if (res.body.errors) { return res.inviteBatch;
throw new Error(res.body.errors[0].message);
}
return res.body.data.inviteBatch;
} }
export async function getInviteLink( export async function getInviteLink(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string workspaceId: string
): Promise<{ link: string; expireTime: string }> { ): Promise<{ link: string; expireTime: string }> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
query { query {
workspace(id: "${workspaceId}") { workspace(id: "${workspaceId}") {
inviteLink { inviteLink {
@@ -82,179 +54,103 @@ export async function getInviteLink(
} }
} }
} }
`, `);
})
.expect(200); return res.workspace.inviteLink;
if (res.body.errors) {
throw new Error(res.body.errors[0].message);
}
return res.body.data.workspace.inviteLink;
} }
export async function createInviteLink( export async function createInviteLink(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
expireTime: 'OneDay' | 'ThreeDays' | 'OneWeek' | 'OneMonth' expireTime: 'OneDay' | 'ThreeDays' | 'OneWeek' | 'OneMonth'
): Promise<{ link: string; expireTime: string }> { ): Promise<{ link: string; expireTime: string }> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
createInviteLink(workspaceId: "${workspaceId}", expireTime: ${expireTime}) { createInviteLink(workspaceId: "${workspaceId}", expireTime: ${expireTime}) {
link link
expireTime expireTime
} }
} }
`, `);
})
.expect(200); return res.createInviteLink;
if (res.body.errors) {
throw new Error(res.body.errors[0].message);
}
return res.body.data.createInviteLink;
} }
export async function revokeInviteLink( export async function revokeInviteLink(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string workspaceId: string
): Promise<boolean> { ): Promise<boolean> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
revokeInviteLink(workspaceId: "${workspaceId}") revokeInviteLink(workspaceId: "${workspaceId}")
} }
`, `);
})
.expect(200); return res.revokeInviteLink;
if (res.body.errors) {
throw new Error(res.body.errors[0].message);
}
return res.body.data.revokeInviteLink;
} }
export async function acceptInviteById( export async function acceptInviteById(
app: INestApplication, app: TestingApp,
workspaceId: string, workspaceId: string,
inviteId: string, inviteId: string,
sendAcceptMail = false, sendAcceptMail = false
token: string = ''
): Promise<boolean> { ): Promise<boolean> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.auth(token, { type: 'bearer' })
.send({
query: `
mutation { mutation {
acceptInviteById(workspaceId: "${workspaceId}", inviteId: "${inviteId}", sendAcceptMail: ${sendAcceptMail}) acceptInviteById(workspaceId: "${workspaceId}", inviteId: "${inviteId}", sendAcceptMail: ${sendAcceptMail})
} }
`, `);
})
.expect(200); return res.acceptInviteById;
if (res.body.errors) {
throw new Error(res.body.errors[0].message, {
cause: res.body.errors[0].cause,
});
}
return res.body.data.acceptInviteById;
} }
export async function approveMember( export async function approveMember(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
userId: string userId: string
): Promise<string> { ): Promise<string> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.auth(token, { type: 'bearer' })
.send({
query: `
mutation { mutation {
approveMember(workspaceId: "${workspaceId}", userId: "${userId}") approveMember(workspaceId: "${workspaceId}", userId: "${userId}")
} }
`, `);
})
.expect(200); return res.approveMember;
if (res.body.errors) {
throw new Error(res.body.errors[0].message, {
cause: res.body.errors[0].cause,
});
}
return res.body.data.approveMember;
} }
export async function leaveWorkspace( export async function leaveWorkspace(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
sendLeaveMail = false sendLeaveMail = false
): Promise<boolean> { ): Promise<boolean> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
leaveWorkspace(workspaceId: "${workspaceId}", sendLeaveMail: ${sendLeaveMail}) leaveWorkspace(workspaceId: "${workspaceId}", sendLeaveMail: ${sendLeaveMail})
} }
`, `);
})
.expect(200); return res.leaveWorkspace;
if (res.body.errors) {
throw new Error(res.body.errors[0].message);
}
return res.body.data.leaveWorkspace;
} }
export async function revokeUser( export async function revokeUser(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
userId: string userId: string
): Promise<boolean> { ): Promise<boolean> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
revoke(workspaceId: "${workspaceId}", userId: "${userId}") revoke(workspaceId: "${workspaceId}", userId: "${userId}")
} }
`, `);
})
.expect(200); return res.revoke;
if (res.body.errors) {
throw new Error(res.body.errors[0].message, {
cause: res.body.errors[0].cause,
});
}
return res.body.data.revoke;
} }
export async function getInviteInfo( export async function getInviteInfo(
app: INestApplication, app: TestingApp,
token: string,
inviteId: string inviteId: string
): Promise<InvitationType> { ): Promise<InvitationType> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
query { query {
getInviteInfo(inviteId: "${inviteId}") { getInviteInfo(inviteId: "${inviteId}") {
workspace { workspace {
@@ -269,13 +165,7 @@ export async function getInviteInfo(
} }
} }
} }
`, `);
})
.expect(200); return res.getInviteInfo;
if (res.body.errors) {
throw new Error(res.body.errors[0].message, {
cause: res.body.errors[0].cause,
});
}
return res.body.data.getInviteInfo;
} }

View File

@@ -1,23 +1,14 @@
import type { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { DocRole } from '../../core/permission/types'; import { DocRole } from '../../core/permission/types';
import { gql } from './common'; import { TestingApp } from './testing-app';
export function grantDocUserRoles( export async function grantDocUserRoles(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
docId: string, docId: string,
userIds: string[], userIds: string[],
role: DocRole role: DocRole
) { ) {
return request(app.getHttpServer()) return await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
grantDocUserRoles(input: { grantDocUserRoles(input: {
workspaceId: "${workspaceId}", workspaceId: "${workspaceId}",
@@ -26,23 +17,16 @@ export function grantDocUserRoles(
role: ${DocRole[role]} role: ${DocRole[role]}
}) })
} }
`, `);
});
} }
export function revokeDocUserRoles( export async function revokeDocUserRoles(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
docId: string, docId: string,
userId: string userId: string
) { ) {
return request(app.getHttpServer()) return await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
revokeDocUserRoles(input: { revokeDocUserRoles(input: {
workspaceId: "${workspaceId}", workspaceId: "${workspaceId}",
@@ -50,23 +34,16 @@ export function revokeDocUserRoles(
userId: "${userId}" userId: "${userId}"
}) })
} }
`, `);
});
} }
export function updateDocDefaultRole( export async function updateDocDefaultRole(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
docId: string, docId: string,
role: DocRole role: DocRole
) { ) {
return request(app.getHttpServer()) return await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
updateDocDefaultRole(input: { updateDocDefaultRole(input: {
workspaceId: "${workspaceId}", workspaceId: "${workspaceId}",
@@ -74,24 +51,17 @@ export function updateDocDefaultRole(
role: ${DocRole[role]} role: ${DocRole[role]}
}) })
} }
`, `);
});
} }
export async function docGrantedUsersList( export async function docGrantedUsersList(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
docId: string, docId: string,
first = 10, first = 10,
offset = 0 offset = 0
) { ) {
const res = await request(app.getHttpServer()) return await app.gql(`
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
query { query {
workspace(id: "${workspaceId}") { workspace(id: "${workspaceId}") {
doc(docId: "${docId}") { doc(docId: "${docId}") {
@@ -110,7 +80,5 @@ export async function docGrantedUsersList(
} }
} }
} }
`, `);
});
return res.body;
} }

View File

@@ -0,0 +1,243 @@
import {
ConsoleLogger,
INestApplication,
ModuleMetadata,
} from '@nestjs/common';
import { TestingModuleBuilder } from '@nestjs/testing';
import { User } from '@prisma/client';
import cookieParser from 'cookie-parser';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import supertest from 'supertest';
import { ApplyType, GlobalExceptionFilter } from '../../base';
import { AuthService } from '../../core/auth';
import { UserModel } from '../../models';
import { createTestingModule } from './testing-module';
import { initTestingDB, TEST_LOG_LEVEL } from './utils';
interface TestingAppMeatdata extends ModuleMetadata {
tapModule?(m: TestingModuleBuilder): void;
tapApp?(app: INestApplication): void;
}
export type TestUser = Omit<User, 'password'> & { password: string };
export async function createTestingApp(
moduleDef: TestingAppMeatdata = {}
): Promise<TestingApp> {
const module = await createTestingModule(moduleDef, false);
const app = module.createNestApplication({
cors: true,
bodyParser: true,
rawBody: true,
});
const logger = new ConsoleLogger();
logger.setLogLevels([TEST_LOG_LEVEL]);
app.useLogger(logger);
app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));
app.use(
graphqlUploadExpress({
maxFileSize: 10 * 1024 * 1024,
maxFiles: 5,
})
);
app.use(cookieParser());
if (moduleDef.tapApp) {
moduleDef.tapApp(app);
}
await module.initTestingDB();
await app.init();
return makeTestingApp(app);
}
export function parseCookies(res: supertest.Response) {
const cookies = res.get('Set-Cookie') ?? [];
const sessionCookie = cookies.reduce(
(cookies, cookie) => {
const [key, value] = cookie.split(';')[0].split('=');
cookies[key] = value;
return cookies;
},
{} as Record<string, string>
);
return sessionCookie;
}
export class TestingApp extends ApplyType<INestApplication>() {
private sessionCookie: string | null = null;
private currentUserCookie: string | null = null;
private readonly userCookies: Set<string> = new Set();
[Symbol.asyncDispose](): Promise<void> {
return this.close();
}
async initTestingDB() {
await initTestingDB(this);
this.sessionCookie = null;
this.currentUserCookie = null;
this.userCookies.clear();
}
url() {
const server = this.getHttpServer();
if (!server.address()) {
server.listen();
}
return `http://localhost:${server.address().port}`;
}
request(
method: 'get' | 'post' | 'put' | 'delete' | 'patch',
path: string
): supertest.Test {
return supertest(this.getHttpServer())
[method](path)
.set('Cookie', [
`${AuthService.sessionCookieName}=${this.sessionCookie ?? ''}`,
`${AuthService.userCookieName}=${this.currentUserCookie ?? ''}`,
]);
}
GET(path: string): supertest.Test {
return this.request('get', path);
}
POST(path: string): supertest.Test {
return this.request('post', path).on(
'response',
(res: supertest.Response) => {
const cookies = parseCookies(res);
if (AuthService.sessionCookieName in cookies) {
if (this.sessionCookie !== cookies[AuthService.sessionCookieName]) {
this.userCookies.clear();
}
this.sessionCookie = cookies[AuthService.sessionCookieName];
this.currentUserCookie = cookies[AuthService.userCookieName];
if (this.currentUserCookie) {
this.userCookies.add(this.currentUserCookie);
}
}
return res;
}
);
}
PUT(path: string): supertest.Test {
return this.request('put', path);
}
DELETE(path: string): supertest.Test {
return this.request('delete', path);
}
PATCH(path: string): supertest.Test {
return this.request('patch', path);
}
// TODO(@forehalo): directly make proxy for graphql queries defined in `@affine/graphql`
// by calling with `app.apis.createWorkspace({ ...variables })`
async gql<Data = any>(query: string, variables?: any): Promise<Data> {
const res = await this.POST('/graphql')
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query,
variables,
})
.expect(200);
if (res.body.errors?.length) {
throw new Error(res.body.errors[0].message);
}
return res.body.data;
}
async createUser(email: string, override?: Partial<User>): Promise<TestUser> {
const model = this.get(UserModel);
// TODO(@forehalo): model factories
// TestingData.user.create()
const user = await model.create({
email,
password: '1',
name: email,
emailVerifiedAt: new Date(),
...override,
});
// returned password is not encrypted
user.password = '1';
return user as Omit<User, 'password'> & { password: string };
}
async signup(email: string, override?: Partial<User>) {
const user = await this.createUser(email, override);
await this.login(user);
return user;
}
async login(user: TestUser) {
await this.POST('/api/auth/sign-in')
.send({
email: user.email,
password: user.password,
})
.expect(200);
}
async switchUser(userOrId: string | { id: string }) {
if (!this.sessionCookie) {
throw new Error('No user is logged in.');
}
const userId = typeof userOrId === 'string' ? userOrId : userOrId.id;
if (userId === this.currentUserCookie) {
return;
}
if (this.userCookies.has(userId)) {
this.currentUserCookie = userId;
} else {
throw new Error(`User [${userId}] is not logged in.`);
}
}
async logout(userId?: string) {
const res = await this.GET(
'/api/auth/sign-out' + (userId ? `?user_id=${userId}` : '')
).expect(200);
const cookies = parseCookies(res);
this.sessionCookie = cookies[AuthService.sessionCookieName];
if (!this.sessionCookie) {
this.currentUserCookie = null;
this.userCookies.clear();
} else {
this.currentUserCookie = cookies[AuthService.userCookieName];
if (userId) {
this.userCookies.delete(userId);
}
}
}
}
function makeTestingApp(app: INestApplication): TestingApp {
const testingApp = new TestingApp();
return new Proxy(testingApp, {
get(target, prop) {
// @ts-expect-error override
return target[prop] ?? app[prop];
},
});
}

View File

@@ -0,0 +1,110 @@
import { ModuleMetadata } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { Query, Resolver } from '@nestjs/graphql';
import {
Test,
TestingModule as BaseTestingModule,
TestingModuleBuilder,
} from '@nestjs/testing';
import { AppModule, FunctionalityModules } from '../../app.module';
import { Runtime } from '../../base';
import { GqlModule } from '../../base/graphql';
import { AuthGuard, AuthModule } from '../../core/auth';
import { ModelsModule } from '../../models';
import { initTestingDB, TEST_LOG_LEVEL } from './utils';
interface TestingModuleMeatdata extends ModuleMetadata {
tapModule?(m: TestingModuleBuilder): void;
}
export interface TestingModule extends BaseTestingModule {
initTestingDB(): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
}
function dedupeModules(modules: NonNullable<ModuleMetadata['imports']>) {
const map = new Map();
modules.forEach(m => {
if ('module' in m) {
map.set(m.module, m);
} else {
map.set(m, m);
}
});
return Array.from(map.values());
}
@Resolver(() => String)
class MockResolver {
@Query(() => String)
hello() {
return 'hello world';
}
}
export async function createTestingModule(
moduleDef: TestingModuleMeatdata = {},
autoInitialize = true
): Promise<TestingModule> {
// setting up
let imports = moduleDef.imports ?? [AppModule];
imports =
imports[0] === AppModule
? [AppModule]
: dedupeModules([
...FunctionalityModules,
ModelsModule,
AuthModule,
GqlModule,
...imports,
]);
const builder = Test.createTestingModule({
imports,
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
MockResolver,
...(moduleDef.providers ?? []),
],
controllers: moduleDef.controllers,
});
if (moduleDef.tapModule) {
moduleDef.tapModule(builder);
}
const module = await builder.compile();
const testingModule = module as TestingModule;
testingModule.initTestingDB = async () => {
await initTestingDB(module);
const runtime = module.get(Runtime);
// by pass password min length validation
await runtime.set('auth/password.min', 1);
};
testingModule[Symbol.asyncDispose] = async () => {
await module.close();
};
if (autoInitialize) {
// we got a lot smoking tests try to break nestjs
// can't tolerate the noisy logs
// @ts-expect-error private
module.applyLogger({
logger: [TEST_LOG_LEVEL],
});
await testingModule.initTestingDB();
await testingModule.init();
}
return testingModule;
}

View File

@@ -1,191 +1,81 @@
import type { INestApplication } from '@nestjs/common'; import { TestingApp } from './testing-app';
import request, { type Response } from 'supertest';
import { export async function currentUser(app: TestingApp) {
AuthService, const res = await app.gql(`
type ClientTokenType,
type CurrentUser,
} from '../../core/auth';
import { sessionUser } from '../../core/auth/service';
import { UserType } from '../../core/user';
import { Models } from '../../models';
import { gql } from './common';
export type UserAuthedType = UserType & { token: ClientTokenType };
export async function internalSignIn(app: INestApplication, userId: string) {
const auth = app.get(AuthService);
const session = await auth.createUserSession(userId);
return `${AuthService.sessionCookieName}=${session.sessionId}`;
}
export function sessionCookie(headers: any): string {
const cookie = headers['set-cookie']?.find((c: string) =>
c.startsWith(`${AuthService.sessionCookieName}=`)
);
if (!cookie) {
return '';
}
return cookie.split(';')[0];
}
export async function getSession(
app: INestApplication,
signInRes: Response
): Promise<{ user?: CurrentUser }> {
const cookie = sessionCookie(signInRes.headers);
const res = await request(app.getHttpServer())
.get('/api/auth/session')
.set('cookie', cookie!)
.expect(200);
return res.body;
}
export async function signUp(
app: INestApplication,
name: string,
email: string,
password: string,
autoVerifyEmail = true
): Promise<UserAuthedType> {
const user = await app.get(Models).user.create({
name,
email,
password,
emailVerifiedAt: autoVerifyEmail ? new Date() : null,
});
const { sessionId } = await app.get(AuthService).createUserSession(user.id);
return {
...sessionUser(user),
token: { token: sessionId, refresh: '' },
};
}
export async function currentUser(app: INestApplication, token: string) {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
query { query {
currentUser { currentUser {
id, name, email, emailVerified, avatarUrl, hasPassword, id, name, email, emailVerified, avatarUrl, hasPassword,
token { token } token { token }
} }
} }
`, `);
}) return res.currentUser;
.expect(200);
return res.body.data.currentUser;
} }
export async function sendChangeEmail( export async function sendChangeEmail(
app: INestApplication, app: TestingApp,
userToken: string,
email: string, email: string,
callbackUrl: string callbackUrl: string
): Promise<boolean> { ): Promise<boolean> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
sendChangeEmail(email: "${email}", callbackUrl: "${callbackUrl}") sendChangeEmail(email: "${email}", callbackUrl: "${callbackUrl}")
} }
`, `);
})
.expect(200);
return res.body.data.sendChangeEmail; return res.sendChangeEmail;
} }
export async function sendSetPasswordEmail( export async function sendSetPasswordEmail(
app: INestApplication, app: TestingApp,
userToken: string,
email: string, email: string,
callbackUrl: string callbackUrl: string
): Promise<boolean> { ): Promise<boolean> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
sendSetPasswordEmail(email: "${email}", callbackUrl: "${callbackUrl}") sendSetPasswordEmail(email: "${email}", callbackUrl: "${callbackUrl}")
} }
`, `);
})
.expect(200);
return res.body.data.sendChangeEmail; return res.sendSetPasswordEmail;
} }
export async function changePassword( export async function changePassword(
app: INestApplication, app: TestingApp,
userId: string, userId: string,
token: string, token: string,
password: string password: string
): Promise<string> { ): Promise<string> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql) mutation {
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) changePassword(token: "${token}", userId: "${userId}", newPassword: "${password}")
.send({
query: `
mutation changePassword($token: String!, $userId: String!, $password: String!) {
changePassword(token: $token, userId: $userId, newPassword: $password)
} }
`, `);
variables: { token, password, userId },
}) return res.changePassword;
.expect(200);
return res.body.data.changePassword;
} }
export async function sendVerifyChangeEmail( export async function sendVerifyChangeEmail(
app: INestApplication, app: TestingApp,
userToken: string,
token: string, token: string,
email: string, email: string,
callbackUrl: string callbackUrl: string
): Promise<boolean> { ): Promise<boolean> {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
sendVerifyChangeEmail(token:"${token}", email: "${email}", callbackUrl: "${callbackUrl}") sendVerifyChangeEmail(token: "${token}", email: "${email}", callbackUrl: "${callbackUrl}")
} }
`, `);
})
.expect(200);
return res.body.data.sendVerifyChangeEmail; return res.sendVerifyChangeEmail;
} }
export async function changeEmail( export async function changeEmail(
app: INestApplication, app: TestingApp,
userToken: string,
token: string, token: string,
email: string email: string
): Promise<UserAuthedType> { ) {
const res = await request(app.getHttpServer()) const res = await app.gql(`
.post(gql)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
changeEmail(token: "${token}", email: "${email}") { changeEmail(token: "${token}", email: "${email}") {
id id
@@ -194,8 +84,41 @@ export async function changeEmail(
email email
} }
} }
`, `);
})
.expect(200); return res.changeEmail;
return res.body.data.changeEmail; }
export async function deleteAccount(app: TestingApp) {
const res = await app.gql(`
mutation {
deleteAccount {
success
}
}
`);
return res.deleteAccount.success;
}
export async function updateAvatar(app: TestingApp, avatar: Buffer) {
return app
.POST('/graphql')
.field(
'operations',
JSON.stringify({
name: 'uploadAvatar',
query: `mutation uploadAvatar($avatar: Upload!) {
uploadAvatar(avatar: $avatar) {
avatarUrl
}
}`,
variables: { avatar: null },
})
)
.field('map', JSON.stringify({ '0': ['variables.avatar'] }))
.attach('0', avatar, {
filename: 'test.png',
contentType: 'image/png',
});
} }

View File

@@ -1,25 +1,10 @@
import { INestApplication, LogLevel, ModuleMetadata } from '@nestjs/common'; import { INestApplicationContext, LogLevel } from '@nestjs/common';
import { APP_GUARD, ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { Query, Resolver } from '@nestjs/graphql';
import {
Test,
TestingModule as BaseTestingModule,
TestingModuleBuilder,
} from '@nestjs/testing';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import cookieParser from 'cookie-parser';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import type { Response } from 'supertest';
import supertest from 'supertest';
import { AppModule, FunctionalityModules } from '../../app.module';
import { AFFiNELogger, GlobalExceptionFilter, Runtime } from '../../base';
import { GqlModule } from '../../base/graphql';
import { AuthGuard, AuthModule } from '../../core/auth';
import { RefreshFeatures0001 } from '../../data/migrations/0001-refresh-features'; import { RefreshFeatures0001 } from '../../data/migrations/0001-refresh-features';
import { ModelsModule } from '../../models';
const TEST_LOG_LEVEL: LogLevel = export const TEST_LOG_LEVEL: LogLevel =
(process.env.TEST_LOG_LEVEL as LogLevel) ?? 'fatal'; (process.env.TEST_LOG_LEVEL as LogLevel) ?? 'fatal';
async function flushDB(client: PrismaClient) { async function flushDB(client: PrismaClient) {
@@ -38,190 +23,10 @@ async function flushDB(client: PrismaClient) {
); );
} }
interface TestingModuleMetadata extends ModuleMetadata { export async function initTestingDB(context: INestApplicationContext) {
tapModule?(m: TestingModuleBuilder): void; const db = context.get(PrismaClient, { strict: false });
tapApp?(app: INestApplication): void;
}
const initTestingDB = async (ref: ModuleRef) => {
const db = ref.get(PrismaClient, { strict: false });
await flushDB(db); await flushDB(db);
await RefreshFeatures0001.up(db, ref); await RefreshFeatures0001.up(db, context.get(ModuleRef));
};
export type TestingModule = BaseTestingModule & {
initTestingDB(): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
};
export type TestingApp = INestApplication & {
initTestingDB(): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
// get the url of the http server, e.g. http://localhost:random-port
getHttpServerUrl(): string;
};
function dedupeModules(modules: NonNullable<ModuleMetadata['imports']>) {
const map = new Map();
modules.forEach(m => {
if ('module' in m) {
map.set(m.module, m);
} else {
map.set(m, m);
}
});
return Array.from(map.values());
}
@Resolver(() => String)
class MockResolver {
@Query(() => String)
hello() {
return 'hello world';
}
}
export async function createTestingModule(
moduleDef: TestingModuleMetadata = {},
autoInitialize = true
): Promise<TestingModule> {
// setting up
let imports = moduleDef.imports ?? [];
imports =
imports[0] === AppModule
? [AppModule]
: dedupeModules([
...FunctionalityModules,
ModelsModule,
AuthModule,
GqlModule,
...imports,
]);
const builder = Test.createTestingModule({
imports,
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
MockResolver,
...(moduleDef.providers ?? []),
],
controllers: moduleDef.controllers,
});
if (moduleDef.tapModule) {
moduleDef.tapModule(builder);
}
const m = await builder.compile();
const testingModule = m as TestingModule;
testingModule.initTestingDB = async () => {
await initTestingDB(m.get(ModuleRef));
// we got a lot smoking tests try to break nestjs
// can't tolerate the noisy logs
// @ts-expect-error private
m.applyLogger({
logger: [TEST_LOG_LEVEL],
});
const runtime = m.get(Runtime);
// by pass password min length validation
await runtime.set('auth/password.min', 1);
};
testingModule[Symbol.asyncDispose] = async () => {
await m.close();
};
if (autoInitialize) {
await testingModule.initTestingDB();
await testingModule.init();
}
return testingModule;
}
export async function createTestingApp(
moduleDef: TestingModuleMetadata = {}
): Promise<{ module: TestingModule; app: TestingApp }> {
const m = await createTestingModule(moduleDef, false);
const app = m.createNestApplication({
cors: true,
bodyParser: true,
rawBody: true,
}) as TestingApp;
const logger = new AFFiNELogger();
logger.setLogLevels([TEST_LOG_LEVEL]);
app.useLogger(logger);
app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));
app.use(
graphqlUploadExpress({
maxFileSize: 10 * 1024 * 1024,
maxFiles: 5,
})
);
app.use(cookieParser());
if (moduleDef.tapApp) {
moduleDef.tapApp(app);
}
await m.initTestingDB();
await app.init();
app.initTestingDB = m.initTestingDB.bind(m);
app[Symbol.asyncDispose] = async () => {
await m[Symbol.asyncDispose]();
await app.close();
};
app.getHttpServerUrl = () => {
const server = app.getHttpServer();
if (!server.address()) {
server.listen();
}
return `http://localhost:${server.address().port}`;
};
return {
module: m,
app: app,
};
}
export function handleGraphQLError(resp: Response) {
const { errors } = resp.body;
if (errors) {
const cause = errors[0];
const stacktrace = cause.extensions?.stacktrace;
throw new Error(
stacktrace
? Array.isArray(stacktrace)
? stacktrace.join('\n')
: String(stacktrace)
: cause.message,
cause
);
}
}
export function gql(app: INestApplication, query?: string) {
const req = supertest(app.getHttpServer())
.post('/graphql')
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' });
if (query) {
return req.send({ query });
}
return req;
} }
export async function sleep(ms: number) { export async function sleep(ms: number) {

View File

@@ -1,18 +1,14 @@
import type { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { WorkspaceRole } from '../../core/permission/types'; import { WorkspaceRole } from '../../core/permission/types';
import type { WorkspaceType } from '../../core/workspaces'; import type { WorkspaceType } from '../../core/workspaces';
import { gql } from './common'; import { TestingApp } from './testing-app';
export async function createWorkspace( export async function createWorkspace(app: TestingApp): Promise<WorkspaceType> {
app: INestApplication, const res = await app
token: string .POST('/graphql')
): Promise<WorkspaceType> { .set({
const res = await request(app.getHttpServer()) 'x-request-id': 'test',
.post(gql) 'x-operation-name': 'test',
.auth(token, { type: 'bearer' }) })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.field( .field(
'operations', 'operations',
JSON.stringify({ JSON.stringify({
@@ -26,119 +22,95 @@ export async function createWorkspace(
}) })
) )
.field('map', JSON.stringify({ '0': ['variables.init'] })) .field('map', JSON.stringify({ '0': ['variables.init'] }))
.attach('0', Buffer.from([0, 0]), 'init.data') .attach('0', Buffer.from([0, 0]), 'init.data');
.expect(200);
return res.body.data.createWorkspace; return res.body.data.createWorkspace;
} }
export async function getWorkspacePublicPages( export async function getWorkspacePublicDocs(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string workspaceId: string
) { ) {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
query { query {
workspace(id: "${workspaceId}") { workspace(id: "${workspaceId}") {
publicPages { publicDocs {
id id
mode mode
} }
} }
} }
`, `
}) );
.expect(200);
return res.body.data.workspace.publicPages; return res.workspace.publicDocs;
} }
export async function getWorkspace( export async function getWorkspace(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
skip = 0, skip = 0,
take = 8 take = 8
): Promise<WorkspaceType> { ): Promise<WorkspaceType> {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
query { query {
workspace(id: "${workspaceId}") { workspace(id: "${workspaceId}") {
id, members(skip: ${skip}, take: ${take}) { id, name, email, permission, inviteId, status } id,
members(skip: ${skip}, take: ${take}) { id, name, email, permission, inviteId, status }
} }
} }
`, `
}) );
.expect(200);
return res.body.data.workspace; return res.workspace;
} }
export async function updateWorkspace( export async function updateWorkspace(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
isPublic: boolean isPublic: boolean
): Promise<boolean> { ): Promise<boolean> {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
updateWorkspace(input: { id: "${workspaceId}", public: ${isPublic} }) { updateWorkspace(input: { id: "${workspaceId}", public: ${isPublic} }) {
public public
} }
} }
`, `
}) );
.expect(200);
return res.body.data.updateWorkspace.public; return res.updateWorkspace.public;
} }
export async function publishDoc( export async function publishDoc(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
docId: string docId: string
) { ) {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
publishDoc(workspaceId: "${workspaceId}", docId: "${docId}") { publishDoc(workspaceId: "${workspaceId}", docId: "${docId}") {
id id
mode mode
} }
} }
`, `
}) );
.expect(200);
return res.body.errors?.[0]?.message || res.body.data?.publishDoc; return res.publishDoc;
} }
export async function revokePublicDoc( export async function revokePublicDoc(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
docId: string docId: string
) { ) {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
revokePublicDoc(workspaceId: "${workspaceId}", docId: "${docId}") { revokePublicDoc(workspaceId: "${workspaceId}", docId: "${docId}") {
id id
@@ -146,25 +118,20 @@ export async function revokePublicDoc(
public public
} }
} }
`, `
}) );
.expect(200);
return res.body.errors?.[0]?.message || res.body.data?.revokePublicDoc; return res.revokePublicDoc;
} }
export async function grantMember( export async function grantMember(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
userId: string, userId: string,
permission: WorkspaceRole permission: WorkspaceRole
) { ) {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
grantMember( grantMember(
workspaceId: "${workspaceId}" workspaceId: "${workspaceId}"
@@ -172,35 +139,24 @@ export async function grantMember(
permission: ${WorkspaceRole[permission]} permission: ${WorkspaceRole[permission]}
) )
} }
`, `
}) );
.expect(200);
if (res.body.errors) { return res.grantMember;
throw new Error(res.body.errors[0].message);
}
return res.body.data?.grantMember;
} }
export async function revokeMember( export async function revokeMember(
app: INestApplication, app: TestingApp,
token: string,
workspaceId: string, workspaceId: string,
userId: string userId: string
) { ) {
const res = await request(app.getHttpServer()) const res = await app.gql(
.post(gql) `
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation { mutation {
revoke(workspaceId: "${workspaceId}", userId: "${userId}") revoke(workspaceId: "${workspaceId}", userId: "${userId}")
} }
`, `
}) );
.expect(200);
if (res.body.errors) { return res.revoke;
throw new Error(res.body.errors[0].message);
}
return res.body.data?.revokeMember;
} }

View File

@@ -6,7 +6,6 @@ import { PrismaClient } from '@prisma/client';
import type { TestFn } from 'ava'; import type { TestFn } from 'ava';
import ava from 'ava'; import ava from 'ava';
import { AppModule } from '../app.module';
import { MailService } from '../base/mailer'; import { MailService } from '../base/mailer';
import { AuthService } from '../core/auth/service'; import { AuthService } from '../core/auth/service';
import { Models } from '../models'; import { Models } from '../models';
@@ -18,7 +17,6 @@ import {
inviteUser, inviteUser,
leaveWorkspace, leaveWorkspace,
revokeUser, revokeUser,
signUp,
TestingApp, TestingApp,
} from './utils'; } from './utils';
@@ -31,9 +29,7 @@ const test = ava as TestFn<{
}>; }>;
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp();
imports: [AppModule],
});
t.context.app = app; t.context.app = app;
t.context.client = app.get(PrismaClient); t.context.client = app.get(PrismaClient);
t.context.auth = app.get(AuthService); t.context.auth = app.get(AuthService);
@@ -51,52 +47,53 @@ test.after.always(async t => {
test('should invite a user', async t => { test('should invite a user', async t => {
const { app } = t.context; const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); const u2 = await app.signup('u2@affine.pro');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
const invite = await inviteUser(app, u1.token.token, workspace.id, u2.email); const invite = await inviteUser(app, workspace.id, u2.email);
t.truthy(invite, 'failed to invite user'); t.truthy(invite, 'failed to invite user');
}); });
test('should leave a workspace', async t => { test('should leave a workspace', async t => {
const { app } = t.context; const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); const u2 = await app.signup('u2@affine.pro');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
const id = await inviteUser(app, u1.token.token, workspace.id, u2.email); const invite = await inviteUser(app, workspace.id, u2.email);
await acceptInviteById(app, workspace.id, id, false);
const leave = await leaveWorkspace(app, u2.token.token, workspace.id); app.switchUser(u2.id);
await acceptInviteById(app, workspace.id, invite);
const leave = await leaveWorkspace(app, workspace.id);
t.pass();
t.true(leave, 'failed to leave workspace'); t.true(leave, 'failed to leave workspace');
}); });
test('should revoke a user', async t => { test('should revoke a user', async t => {
const { app } = t.context; const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); const u2 = await app.signup('u2@affine.pro');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
await inviteUser(app, u1.token.token, workspace.id, u2.email); await inviteUser(app, workspace.id, u2.email);
const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); const currWorkspace = await getWorkspace(app, workspace.id);
t.is(currWorkspace.members.length, 2, 'failed to invite user'); t.is(currWorkspace.members.length, 2, 'failed to invite user');
const revoke = await revokeUser(app, u1.token.token, workspace.id, u2.id); const revoke = await revokeUser(app, workspace.id, u2.id);
t.true(revoke, 'failed to revoke user'); t.true(revoke, 'failed to revoke user');
}); });
test('should create user if not exist', async t => { test('should create user if not exist', async t => {
const { app, models } = t.context; const { app, models } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
await inviteUser(app, u1.token.token, workspace.id, 'u2@affine.pro'); await inviteUser(app, workspace.id, 'u2@affine.pro');
const u2 = await models.user.getUserByEmail('u2@affine.pro'); const u2 = await models.user.getUserByEmail('u2@affine.pro');
t.not(u2, undefined, 'failed to create user'); t.not(u2, undefined, 'failed to create user');
@@ -105,21 +102,23 @@ test('should create user if not exist', async t => {
test('should invite a user by link', async t => { test('should invite a user by link', async t => {
const { app } = t.context; const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); const u2 = await app.signup('u2@affine.pro');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); const u1 = await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
const invite = await inviteUser(app, u1.token.token, workspace.id, u2.email); const invite = await inviteUser(app, workspace.id, u2.email);
app.switchUser(u2.id);
const accept = await acceptInviteById(app, workspace.id, invite); const accept = await acceptInviteById(app, workspace.id, invite);
t.true(accept, 'failed to accept invite'); t.true(accept, 'failed to accept invite');
const invite1 = await inviteUser(app, u1.token.token, workspace.id, u2.email); app.switchUser(u1.id);
const invite1 = await inviteUser(app, workspace.id, u2.email);
t.is(invite, invite1, 'repeat the invitation must return same id'); t.is(invite, invite1, 'repeat the invitation must return same id');
const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); const currWorkspace = await getWorkspace(app, workspace.id);
const currMember = currWorkspace.members.find(u => u.email === u2.email); const currMember = currWorkspace.members.find(u => u.email === u2.email);
t.not(currMember, undefined, 'failed to invite user'); t.not(currMember, undefined, 'failed to invite user');
t.is(currMember?.inviteId, invite, 'failed to check invite id'); t.is(currMember?.inviteId, invite, 'failed to check invite id');
@@ -128,19 +127,13 @@ test('should invite a user by link', async t => {
test('should send email', async t => { test('should send email', async t => {
const { mail, app } = t.context; const { mail, app } = t.context;
if (mail.hasConfigured()) { if (mail.hasConfigured()) {
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); const u2 = await app.signup('u2@affine.pro');
const u2 = await signUp(app, 'test', 'production@toeverything.info', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
const primitiveMailCount = await getCurrentMailMessageCount(); const primitiveMailCount = await getCurrentMailMessageCount();
const invite = await inviteUser( const invite = await inviteUser(app, workspace.id, u2.email, true);
app,
u1.token.token,
workspace.id,
u2.email,
true
);
const afterInviteMailCount = await getCurrentMailMessageCount(); const afterInviteMailCount = await getCurrentMailMessageCount();
t.is( t.is(
@@ -152,12 +145,13 @@ test('should send email', async t => {
t.not( t.not(
inviteEmailContent.To.find((item: any) => { inviteEmailContent.To.find((item: any) => {
return item.Mailbox === 'production'; return item.Mailbox === 'u2';
}), }),
undefined, undefined,
'invite email address was incorrectly sent' 'invite email address was incorrectly sent'
); );
app.switchUser(u2.id);
const accept = await acceptInviteById(app, workspace.id, invite, true); const accept = await acceptInviteById(app, workspace.id, invite, true);
t.true(accept, 'failed to accept invite'); t.true(accept, 'failed to accept invite');
@@ -176,7 +170,7 @@ test('should send email', async t => {
'accept email address was incorrectly sent' 'accept email address was incorrectly sent'
); );
await leaveWorkspace(app, u2.token.token, workspace.id, true); await leaveWorkspace(app, workspace.id, true);
// TODO(@darkskygit): enable this after cluster event system is ready // TODO(@darkskygit): enable this after cluster event system is ready
// const afterLeaveMailCount = await getCurrentMailMessageCount(); // const afterLeaveMailCount = await getCurrentMailMessageCount();
@@ -199,44 +193,28 @@ test('should send email', async t => {
test('should support pagination for member', async t => { test('should support pagination for member', async t => {
const { app } = t.context; const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); await app.signup('u1@affine.pro');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
const u3 = await signUp(app, 'u3', 'u3@affine.pro', '1');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
const invite1 = await inviteUser(app, u1.token.token, workspace.id, u2.email); await inviteUser(app, workspace.id, 'u2@affine.pro');
const invite2 = await inviteUser(app, u1.token.token, workspace.id, u3.email); await inviteUser(app, workspace.id, 'u3@affine.pro');
await acceptInviteById(app, workspace.id, invite1, false); const firstPageWorkspace = await getWorkspace(app, workspace.id, 0, 2);
await acceptInviteById(app, workspace.id, invite2, false);
const firstPageWorkspace = await getWorkspace(
app,
u1.token.token,
workspace.id,
0,
2
);
t.is(firstPageWorkspace.members.length, 2, 'failed to check invite id'); t.is(firstPageWorkspace.members.length, 2, 'failed to check invite id');
const secondPageWorkspace = await getWorkspace( const secondPageWorkspace = await getWorkspace(app, workspace.id, 2, 2);
app,
u1.token.token,
workspace.id,
2,
2
);
t.is(secondPageWorkspace.members.length, 1, 'failed to check invite id'); t.is(secondPageWorkspace.members.length, 1, 'failed to check invite id');
}); });
test('should limit member count correctly', async t => { test('should limit member count correctly', async t => {
const { app } = t.context; const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token);
const workspace = await createWorkspace(app);
await Promise.allSettled( await Promise.allSettled(
Array.from({ length: 10 }).map(async (_, i) => Array.from({ length: 10 }).map(async (_, i) =>
inviteUser(app, u1.token.token, workspace.id, `u${i}@affine.pro`) inviteUser(app, workspace.id, `u${i}@affine.pro`)
) )
); );
const ws = await getWorkspace(app, u1.token.token, workspace.id); const ws = await getWorkspace(app, workspace.id);
t.assert(ws.members.length <= 3, 'failed to check member list'); t.assert(ws.members.length <= 3, 'failed to check member list');
}); });

View File

@@ -1,18 +1,15 @@
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import type { TestFn } from 'ava'; import type { TestFn } from 'ava';
import ava from 'ava'; import ava from 'ava';
import request from 'supertest';
import { AppModule } from '../app.module';
import { import {
acceptInviteById, acceptInviteById,
createTestingApp, createTestingApp,
createWorkspace, createWorkspace,
getWorkspacePublicPages, getWorkspacePublicDocs,
inviteUser, inviteUser,
publishDoc, publishDoc,
revokePublicDoc, revokePublicDoc,
signUp,
TestingApp, TestingApp,
updateWorkspace, updateWorkspace,
} from './utils'; } from './utils';
@@ -23,9 +20,7 @@ const test = ava as TestFn<{
}>; }>;
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp();
imports: [AppModule],
});
t.context.client = app.get(PrismaClient); t.context.client = app.get(PrismaClient);
t.context.app = app; t.context.app = app;
@@ -39,134 +34,99 @@ test.after.always(async t => {
await t.context.app.close(); await t.context.app.close();
}); });
test('should register a user', async t => {
const user = await signUp(t.context.app, 'u1', 'u1@affine.pro', '123456');
t.is(typeof user.id, 'string', 'user.id is not a string');
t.is(user.name, 'u1', 'user.name is not valid');
t.is(user.email, 'u1@affine.pro', 'user.email is not valid');
});
test('should create a workspace', async t => { test('should create a workspace', async t => {
const { app } = t.context; const { app } = t.context;
const user = await signUp(app, 'u1', 'u1@affine.pro', '1');
const workspace = await createWorkspace(app, user.token.token); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app);
t.is(typeof workspace.id, 'string', 'workspace.id is not a string'); t.is(typeof workspace.id, 'string', 'workspace.id is not a string');
}); });
test('should be able to publish workspace', async t => { test('should be able to publish workspace', async t => {
const { app } = t.context; const { app } = t.context;
const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, user.token.token); const workspace = await createWorkspace(app);
const isPublic = await updateWorkspace(app, workspace.id, true);
const isPublic = await updateWorkspace(
app,
user.token.token,
workspace.id,
true
);
t.true(isPublic, 'failed to publish workspace'); t.true(isPublic, 'failed to publish workspace');
const isPrivate = await updateWorkspace( const isPrivate = await updateWorkspace(app, workspace.id, false);
app,
user.token.token,
workspace.id,
false
);
t.false(isPrivate, 'failed to unpublish workspace'); t.false(isPrivate, 'failed to unpublish workspace');
}); });
test('should share a page', async t => { test('should visit public page', async t => {
const { app } = t.context; const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); await app.signup('u1@affine.pro');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
const share = await publishDoc(app, workspace.id, 'doc1');
const share = await publishDoc(app, u1.token.token, workspace.id, 'doc1');
t.is(share.id, 'doc1', 'failed to share doc'); t.is(share.id, 'doc1', 'failed to share doc');
const pages = await getWorkspacePublicPages(
app, const docs = await getWorkspacePublicDocs(app, workspace.id);
u1.token.token, t.is(docs.length, 1, 'failed to get shared docs');
workspace.id
);
t.is(pages.length, 1, 'failed to get shared pages');
t.deepEqual( t.deepEqual(
pages[0], docs[0],
{ id: 'doc1', mode: 'Page' }, { id: 'doc1', mode: 'Page' },
'failed to get shared doc: doc1' 'failed to get shared doc: doc1'
); );
const resp1 = await request(app.getHttpServer()) const resp1 = await app.GET(
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) `/api/workspaces/${workspace.id}/docs/${workspace.id}`
.auth(u1.token.token, { type: 'bearer' }); );
t.is(resp1.statusCode, 200, 'failed to get root doc with u1 token'); t.is(resp1.statusCode, 200, 'failed to get root doc with u1 token');
const resp2 = await request(app.getHttpServer()).get( const resp2 = await app.GET(
`/api/workspaces/${workspace.id}/docs/${workspace.id}` `/api/workspaces/${workspace.id}/docs/${workspace.id}`
); );
t.is(resp2.statusCode, 200, 'failed to get root doc with public pages'); t.is(resp2.statusCode, 200, 'failed to get root doc with public pages');
const resp3 = await request(app.getHttpServer()) const resp3 = await app.GET(`/api/workspaces/${workspace.id}/docs/doc1`);
.get(`/api/workspaces/${workspace.id}/docs/doc1`)
.auth(u1.token.token, { type: 'bearer' });
// 404 because we don't put the page doc to server // 404 because we don't put the page doc to server
t.is(resp3.statusCode, 404, 'failed to get shared doc with u1 token'); t.is(resp3.statusCode, 404, 'failed to get shared doc with u1 token');
const resp4 = await request(app.getHttpServer()).get( const resp4 = await app.GET(`/api/workspaces/${workspace.id}/docs/doc1`);
`/api/workspaces/${workspace.id}/docs/doc1`
);
// 404 because we don't put the page doc to server // 404 because we don't put the page doc to server
t.is(resp4.statusCode, 404, 'should not get shared doc without token'); t.is(resp4.statusCode, 404, 'should not get shared doc without token');
const msg1 = await publishDoc(app, u2.token.token, 'not_exists_ws', 'doc2'); const revoke = await revokePublicDoc(app, workspace.id, 'doc1');
t.is(
msg1,
'You do not have permission to access doc doc2 under Space not_exists_ws.',
'unauthorized user can share doc'
);
const msg2 = await revokePublicDoc(
app,
u2.token.token,
'not_exists_ws',
'doc2'
);
t.is(
msg2,
'You do not have permission to access doc doc2 under Space not_exists_ws.',
'unauthorized user can share doc'
);
const revoke = await revokePublicDoc(
app,
u1.token.token,
workspace.id,
'doc1'
);
t.false(revoke.public, 'failed to revoke doc'); t.false(revoke.public, 'failed to revoke doc');
const pages2 = await getWorkspacePublicPages( const docs2 = await getWorkspacePublicDocs(app, workspace.id);
app, t.is(docs2.length, 0, 'failed to get shared docs');
u1.token.token, await t.throwsAsync(revokePublicDoc(app, workspace.id, 'doc3'), {
workspace.id message: 'Doc is not public',
); });
t.is(pages2.length, 0, 'failed to get shared pages');
const msg4 = await revokePublicDoc(app, u1.token.token, workspace.id, 'doc3');
t.is(msg4, 'Doc is not public');
const pages3 = await getWorkspacePublicPages( const docs3 = await getWorkspacePublicDocs(app, workspace.id);
app, t.is(docs3.length, 0, 'failed to get shared docs');
u1.token.token, });
workspace.id
); test('should not be able to public not permitted doc', async t => {
t.is(pages3.length, 0, 'failed to get shared pages'); const { app } = t.context;
await app.signup('u2@affine.pro');
await t.throwsAsync(publishDoc(app, 'not_exists_ws', 'doc2'), {
message:
'You do not have permission to access doc doc2 under Space not_exists_ws.',
});
await t.throwsAsync(revokePublicDoc(app, 'not_exists_ws', 'doc2'), {
message:
'You do not have permission to access doc doc2 under Space not_exists_ws.',
});
}); });
test('should be able to get workspace doc', async t => { test('should be able to get workspace doc', async t => {
const { app } = t.context; const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); const u1 = await app.signup('u1@affine.pro');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '2'); const u2 = await app.signup('u2@affine.pro');
const workspace = await createWorkspace(app, u1.token.token);
const res1 = await request(app.getHttpServer()) app.switchUser(u1.id);
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) const workspace = await createWorkspace(app);
.auth(u1.token.token, { type: 'bearer' })
const res1 = await app
.GET(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
.expect(200) .expect(200)
.type('application/octet-stream'); .type('application/octet-stream');
@@ -176,28 +136,25 @@ test('should be able to get workspace doc', async t => {
'failed to get doc with u1 token' 'failed to get doc with u1 token'
); );
await request(app.getHttpServer()) app.switchUser(u2.id);
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) await app
.GET(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
.expect(403); .expect(403);
await request(app.getHttpServer()) await app
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) .GET(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
.auth(u2.token.token, { type: 'bearer' })
.expect(403); .expect(403);
await request(app.getHttpServer()) await app
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) .GET(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
.auth(u2.token.token, { type: 'bearer' })
.expect(403); .expect(403);
await acceptInviteById( await app.switchUser(u1.id);
app, const invite = await inviteUser(app, workspace.id, u2.email);
workspace.id, await app.switchUser(u2.id);
await inviteUser(app, u1.token.token, workspace.id, u2.email) await acceptInviteById(app, workspace.id, invite);
);
const res2 = await request(app.getHttpServer()) const res2 = await app
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) .GET(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
.auth(u2.token.token, { type: 'bearer' })
.expect(200) .expect(200)
.type('application/octet-stream'); .type('application/octet-stream');
@@ -210,20 +167,15 @@ test('should be able to get workspace doc', async t => {
test('should be able to get public workspace doc', async t => { test('should be able to get public workspace doc', async t => {
const { app } = t.context; const { app } = t.context;
const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, user.token.token);
const isPublic = await updateWorkspace( const workspace = await createWorkspace(app);
app, const isPublic = await updateWorkspace(app, workspace.id, true);
user.token.token,
workspace.id,
true
);
t.true(isPublic, 'failed to publish workspace'); t.true(isPublic, 'failed to publish workspace');
const res = await request(app.getHttpServer()) const res = await app
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) .GET(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
.expect(200) .expect(200)
.type('application/octet-stream'); .type('application/octet-stream');

View File

@@ -1,7 +1,5 @@
import test from 'ava'; import test from 'ava';
import request from 'supertest';
import { AppModule } from '../../app.module';
import { WorkspaceFeatureModel } from '../../models'; import { WorkspaceFeatureModel } from '../../models';
import { import {
collectAllBlobSizes, collectAllBlobSizes,
@@ -10,7 +8,6 @@ import {
getWorkspaceBlobsSize, getWorkspaceBlobsSize,
listBlobs, listBlobs,
setBlob, setBlob,
signUp,
TestingApp, TestingApp,
} from '../utils'; } from '../utils';
@@ -27,11 +24,7 @@ let app: TestingApp;
let model: WorkspaceFeatureModel; let model: WorkspaceFeatureModel;
test.before(async () => { test.before(async () => {
const { app: testApp } = await createTestingApp({ app = await createTestingApp();
imports: [AppModule],
});
app = testApp;
model = app.get(WorkspaceFeatureModel); model = app.get(WorkspaceFeatureModel);
}); });
@@ -44,117 +37,113 @@ test.after.always(async () => {
}); });
test('should set blobs', async t => { test('should set blobs', async t => {
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
const buffer1 = Buffer.from([0, 0]); const buffer1 = Buffer.from([0, 0]);
const hash1 = await setBlob(app, u1.token.token, workspace.id, buffer1); const hash1 = await setBlob(app, workspace.id, buffer1);
const buffer2 = Buffer.from([0, 1]); const buffer2 = Buffer.from([0, 1]);
const hash2 = await setBlob(app, u1.token.token, workspace.id, buffer2); const hash2 = await setBlob(app, workspace.id, buffer2);
const server = app.getHttpServer(); const response1 = await app
.GET(`/api/workspaces/${workspace.id}/blobs/${hash1}`)
const response1 = await request(server)
.get(`/api/workspaces/${workspace.id}/blobs/${hash1}`)
.auth(u1.token.token, { type: 'bearer' })
.buffer(); .buffer();
t.deepEqual(response1.body, buffer1, 'failed to get blob'); t.deepEqual(response1.body, buffer1, 'failed to get blob');
const response2 = await request(server) const response2 = await app
.get(`/api/workspaces/${workspace.id}/blobs/${hash2}`) .GET(`/api/workspaces/${workspace.id}/blobs/${hash2}`)
.auth(u1.token.token, { type: 'bearer' })
.buffer(); .buffer();
t.deepEqual(response2.body, buffer2, 'failed to get blob'); t.deepEqual(response2.body, buffer2, 'failed to get blob');
}); });
test('should list blobs', async t => { test('should list blobs', async t => {
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
const blobs = await listBlobs(app, u1.token.token, workspace.id); const blobs = await listBlobs(app, workspace.id);
t.is(blobs.length, 0, 'failed to list blobs'); t.is(blobs.length, 0, 'failed to list blobs');
const buffer1 = Buffer.from([0, 0]); const buffer1 = Buffer.from([0, 0]);
const hash1 = await setBlob(app, u1.token.token, workspace.id, buffer1); const hash1 = await setBlob(app, workspace.id, buffer1);
const buffer2 = Buffer.from([0, 1]); const buffer2 = Buffer.from([0, 1]);
const hash2 = await setBlob(app, u1.token.token, workspace.id, buffer2); const hash2 = await setBlob(app, workspace.id, buffer2);
const ret = await listBlobs(app, u1.token.token, workspace.id); const ret = await listBlobs(app, workspace.id);
t.is(ret.length, 2, 'failed to list blobs'); t.is(ret.length, 2, 'failed to list blobs');
// list blob result is not ordered // list blob result is not ordered
t.deepEqual(ret.sort(), [hash1, hash2].sort()); t.deepEqual(ret.sort(), [hash1, hash2].sort());
}); });
test('should calc blobs size', async t => { test('should calc blobs size', async t => {
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
const buffer1 = Buffer.from([0, 0]); const buffer1 = Buffer.from([0, 0]);
await setBlob(app, u1.token.token, workspace.id, buffer1); await setBlob(app, workspace.id, buffer1);
const buffer2 = Buffer.from([0, 1]); const buffer2 = Buffer.from([0, 1]);
await setBlob(app, u1.token.token, workspace.id, buffer2); await setBlob(app, workspace.id, buffer2);
const size = await getWorkspaceBlobsSize(app, u1.token.token, workspace.id); const size = await getWorkspaceBlobsSize(app, workspace.id);
t.is(size, 4, 'failed to collect blob sizes'); t.is(size, 4, 'failed to collect blob sizes');
}); });
test('should calc all blobs size', async t => { test('should calc all blobs size', async t => {
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace1 = await createWorkspace(app, u1.token.token); const workspace1 = await createWorkspace(app);
const buffer1 = Buffer.from([0, 0]); const buffer1 = Buffer.from([0, 0]);
await setBlob(app, u1.token.token, workspace1.id, buffer1); await setBlob(app, workspace1.id, buffer1);
const buffer2 = Buffer.from([0, 1]); const buffer2 = Buffer.from([0, 1]);
await setBlob(app, u1.token.token, workspace1.id, buffer2); await setBlob(app, workspace1.id, buffer2);
const workspace2 = await createWorkspace(app, u1.token.token); const workspace2 = await createWorkspace(app);
const buffer3 = Buffer.from([0, 0]); const buffer3 = Buffer.from([0, 0]);
await setBlob(app, u1.token.token, workspace2.id, buffer3); await setBlob(app, workspace2.id, buffer3);
const buffer4 = Buffer.from([0, 1]); const buffer4 = Buffer.from([0, 1]);
await setBlob(app, u1.token.token, workspace2.id, buffer4); await setBlob(app, workspace2.id, buffer4);
const size = await collectAllBlobSizes(app, u1.token.token); const size = await collectAllBlobSizes(app);
t.is(size, 8, 'failed to collect all blob sizes'); t.is(size, 8, 'failed to collect all blob sizes');
}); });
test('should reject blob exceeded limit', async t => { test('should reject blob exceeded limit', async t => {
const u1 = await signUp(app, 'darksky', 'darksky@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace1 = await createWorkspace(app, u1.token.token); const workspace1 = await createWorkspace(app);
await model.add(workspace1.id, 'team_plan_v1', 'test', RESTRICTED_QUOTA); await model.add(workspace1.id, 'team_plan_v1', 'test', RESTRICTED_QUOTA);
const buffer1 = Buffer.from( const buffer1 = Buffer.from(
Array.from({ length: RESTRICTED_QUOTA.blobLimit + 1 }, () => 0) Array.from({ length: RESTRICTED_QUOTA.blobLimit + 1 }, () => 0)
); );
await t.throwsAsync(setBlob(app, u1.token.token, workspace1.id, buffer1)); await t.throwsAsync(setBlob(app, workspace1.id, buffer1));
}); });
test('should reject blob exceeded quota', async t => { test('should reject blob exceeded quota', async t => {
const u1 = await signUp(app, 'darksky', 'darksky@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
await model.add(workspace.id, 'team_plan_v1', 'test', RESTRICTED_QUOTA); await model.add(workspace.id, 'team_plan_v1', 'test', RESTRICTED_QUOTA);
const buffer = Buffer.from(Array.from({ length: OneMB }, () => 0)); const buffer = Buffer.from(Array.from({ length: OneMB }, () => 0));
await t.notThrowsAsync(setBlob(app, u1.token.token, workspace.id, buffer)); await t.notThrowsAsync(setBlob(app, workspace.id, buffer));
await t.throwsAsync(setBlob(app, u1.token.token, workspace.id, buffer)); await t.throwsAsync(setBlob(app, workspace.id, buffer));
}); });
test('should accept blob even storage out of quota if workspace has unlimited feature', async t => { test('should accept blob even storage out of quota if workspace has unlimited feature', async t => {
const u1 = await signUp(app, 'darksky', 'darksky@affine.pro', '1'); await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app);
await model.add(workspace.id, 'team_plan_v1', 'test', RESTRICTED_QUOTA); await model.add(workspace.id, 'team_plan_v1', 'test', RESTRICTED_QUOTA);
await model.add(workspace.id, 'unlimited_workspace', 'test'); await model.add(workspace.id, 'unlimited_workspace', 'test');
const buffer = Buffer.from(Array.from({ length: OneMB }, () => 0)); const buffer = Buffer.from(Array.from({ length: OneMB }, () => 0));
await t.notThrowsAsync(setBlob(app, u1.token.token, workspace.id, buffer)); await t.notThrowsAsync(setBlob(app, workspace.id, buffer));
await t.notThrowsAsync(setBlob(app, u1.token.token, workspace.id, buffer)); await t.notThrowsAsync(setBlob(app, workspace.id, buffer));
}); });

View File

@@ -1,29 +1,24 @@
import { Readable } from 'node:stream'; import { Readable } from 'node:stream';
import { HttpStatus, INestApplication } from '@nestjs/common'; import { HttpStatus } from '@nestjs/common';
import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client'; import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client';
import ava, { TestFn } from 'ava'; import ava, { TestFn } from 'ava';
import Sinon from 'sinon'; import Sinon from 'sinon';
import request from 'supertest';
import { AppModule } from '../../app.module';
import { CurrentUser } from '../../core/auth';
import { AuthService } from '../../core/auth/service';
import { PgWorkspaceDocStorageAdapter } from '../../core/doc'; import { PgWorkspaceDocStorageAdapter } from '../../core/doc';
import { WorkspaceBlobStorage } from '../../core/storage'; import { WorkspaceBlobStorage } from '../../core/storage';
import { createTestingApp, internalSignIn } from '../utils'; import { createTestingApp, TestingApp, TestUser } from '../utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
u1: CurrentUser;
db: PrismaClient; db: PrismaClient;
app: INestApplication; app: TestingApp;
u1: TestUser;
storage: Sinon.SinonStubbedInstance<WorkspaceBlobStorage>; storage: Sinon.SinonStubbedInstance<WorkspaceBlobStorage>;
workspace: Sinon.SinonStubbedInstance<PgWorkspaceDocStorageAdapter>; workspace: Sinon.SinonStubbedInstance<PgWorkspaceDocStorageAdapter>;
}>; }>;
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [AppModule],
tapModule: m => { tapModule: m => {
m.overrideProvider(WorkspaceBlobStorage) m.overrideProvider(WorkspaceBlobStorage)
.useValue(Sinon.createStubInstance(WorkspaceBlobStorage)) .useValue(Sinon.createStubInstance(WorkspaceBlobStorage))
@@ -32,10 +27,9 @@ test.before(async t => {
}, },
}); });
const auth = app.get(AuthService);
t.context.u1 = await auth.signUp('u1@affine.pro', '1');
const db = app.get(PrismaClient); const db = app.get(PrismaClient);
t.context.u1 = await app.signup('u1@affine.pro');
t.context.db = db; t.context.db = db;
t.context.app = app; t.context.app = app;
t.context.storage = app.get(WorkspaceBlobStorage); t.context.storage = app.get(WorkspaceBlobStorage);
@@ -109,23 +103,19 @@ function blob() {
// blob // blob
test('should be able to get blob from public workspace', async t => { test('should be able to get blob from public workspace', async t => {
const { app, u1, storage } = t.context; const { app, storage } = t.context;
// no authenticated user // no authenticated user
storage.get.resolves(blob()); storage.get.resolves(blob());
let res = await request(t.context.app.getHttpServer()).get( let res = await app.GET('/api/workspaces/public/blobs/test');
'/api/workspaces/public/blobs/test'
);
t.is(res.status, HttpStatus.OK); t.is(res.status, HttpStatus.OK);
t.is(res.get('content-type'), 'text/plain'); t.is(res.get('content-type'), 'text/plain');
t.is(res.text, 'blob'); t.is(res.text, 'blob');
// authenticated user // authenticated user
const cookie = await internalSignIn(app, u1.id); await app.login(t.context.u1);
res = await request(t.context.app.getHttpServer()) res = await app.GET('/api/workspaces/public/blobs/test');
.get('/api/workspaces/public/blobs/test')
.set('Cookie', cookie);
t.is(res.status, HttpStatus.OK); t.is(res.status, HttpStatus.OK);
t.is(res.get('content-type'), 'text/plain'); t.is(res.get('content-type'), 'text/plain');
@@ -133,23 +123,19 @@ test('should be able to get blob from public workspace', async t => {
}); });
test('should be able to get private workspace with public pages', async t => { test('should be able to get private workspace with public pages', async t => {
const { app, u1, storage } = t.context; const { app, storage } = t.context;
// no authenticated user // no authenticated user
storage.get.resolves(blob()); storage.get.resolves(blob());
let res = await request(app.getHttpServer()).get( let res = await app.GET('/api/workspaces/private/blobs/test');
'/api/workspaces/private/blobs/test'
);
t.is(res.status, HttpStatus.OK); t.is(res.status, HttpStatus.OK);
t.is(res.get('content-type'), 'text/plain'); t.is(res.get('content-type'), 'text/plain');
t.is(res.text, 'blob'); t.is(res.text, 'blob');
// authenticated user // authenticated user
const cookie = await internalSignIn(app, u1.id); await app.login(t.context.u1);
res = await request(app.getHttpServer()) res = await app.GET('/api/workspaces/private/blobs/test');
.get('/api/workspaces/private/blobs/test')
.set('cookie', cookie);
t.is(res.status, HttpStatus.OK); t.is(res.status, HttpStatus.OK);
t.is(res.get('content-type'), 'text/plain'); t.is(res.get('content-type'), 'text/plain');
@@ -157,29 +143,24 @@ test('should be able to get private workspace with public pages', async t => {
}); });
test('should not be able to get private workspace with no public pages', async t => { test('should not be able to get private workspace with no public pages', async t => {
const { app, u1 } = t.context; const { app } = t.context;
let res = await request(app.getHttpServer()).get( let res = await app.GET('/api/workspaces/totally-private/blobs/test');
'/api/workspaces/totally-private/blobs/test'
);
t.is(res.status, HttpStatus.FORBIDDEN); t.is(res.status, HttpStatus.FORBIDDEN);
res = await request(app.getHttpServer()) res = await app.GET('/api/workspaces/totally-private/blobs/test');
.get('/api/workspaces/totally-private/blobs/test')
.set('cookie', await internalSignIn(app, u1.id));
t.is(res.status, HttpStatus.FORBIDDEN); t.is(res.status, HttpStatus.FORBIDDEN);
}); });
test('should be able to get permission granted workspace', async t => { test('should be able to get permission granted workspace', async t => {
const { app, u1, db, storage } = t.context; const { app, db, storage } = t.context;
const cookie = await internalSignIn(app, u1.id);
await db.workspaceUserPermission.create({ await db.workspaceUserPermission.create({
data: { data: {
workspaceId: 'totally-private', workspaceId: 'totally-private',
userId: u1.id, userId: t.context.u1.id,
type: 1, type: 1,
accepted: true, accepted: true,
status: WorkspaceMemberStatus.Accepted, status: WorkspaceMemberStatus.Accepted,
@@ -187,9 +168,8 @@ test('should be able to get permission granted workspace', async t => {
}); });
storage.get.resolves(blob()); storage.get.resolves(blob());
const res = await request(app.getHttpServer()) await app.login(t.context.u1);
.get('/api/workspaces/totally-private/blobs/test') const res = await app.GET('/api/workspaces/totally-private/blobs/test');
.set('Cookie', cookie);
t.is(res.status, HttpStatus.OK); t.is(res.status, HttpStatus.OK);
t.is(res.text, 'blob'); t.is(res.text, 'blob');
@@ -200,9 +180,7 @@ test('should return 404 if blob not found', async t => {
// @ts-expect-error mock // @ts-expect-error mock
storage.get.resolves({ body: null }); storage.get.resolves({ body: null });
const res = await request(app.getHttpServer()).get( const res = await app.GET('/api/workspaces/public/blobs/test');
'/api/workspaces/public/blobs/test'
);
t.is(res.status, HttpStatus.NOT_FOUND); t.is(res.status, HttpStatus.NOT_FOUND);
}); });
@@ -210,17 +188,14 @@ test('should return 404 if blob not found', async t => {
// doc // doc
// NOTE: permission checking of doc api is the same with blob api, skip except one // NOTE: permission checking of doc api is the same with blob api, skip except one
test('should not be able to get private workspace with private page', async t => { test('should not be able to get private workspace with private page', async t => {
const { app, u1 } = t.context; const { app } = t.context;
let res = await request(app.getHttpServer()).get( let res = await app.GET('/api/workspaces/private/docs/private-page');
'/api/workspaces/private/docs/private-page'
);
t.is(res.status, HttpStatus.FORBIDDEN); t.is(res.status, HttpStatus.FORBIDDEN);
res = await request(app.getHttpServer()) await app.login(t.context.u1);
.get('/api/workspaces/private/docs/private-page') res = await app.GET('/api/workspaces/private/docs/private-page');
.set('cookie', await internalSignIn(app, u1.id));
t.is(res.status, HttpStatus.FORBIDDEN); t.is(res.status, HttpStatus.FORBIDDEN);
}); });
@@ -235,9 +210,7 @@ test('should be able to get doc', async t => {
timestamp: Date.now(), timestamp: Date.now(),
}); });
const res = await request(app.getHttpServer()).get( const res = await app.GET('/api/workspaces/private/docs/public');
'/api/workspaces/private/docs/public'
);
t.is(res.status, HttpStatus.OK); t.is(res.status, HttpStatus.OK);
t.is(res.get('content-type'), 'application/octet-stream'); t.is(res.get('content-type'), 'application/octet-stream');
@@ -254,9 +227,7 @@ test('should be able to change page publish mode', async t => {
timestamp: Date.now(), timestamp: Date.now(),
}); });
let res = await request(app.getHttpServer()).get( let res = await app.GET('/api/workspaces/private/docs/public');
'/api/workspaces/private/docs/public'
);
t.is(res.status, HttpStatus.OK); t.is(res.status, HttpStatus.OK);
t.is(res.get('publish-mode'), 'page'); t.is(res.get('publish-mode'), 'page');
@@ -266,9 +237,7 @@ test('should be able to change page publish mode', async t => {
data: { mode: 1 }, data: { mode: 1 },
}); });
res = await request(app.getHttpServer()).get( res = await app.GET('/api/workspaces/private/docs/public');
'/api/workspaces/private/docs/public'
);
t.is(res.status, HttpStatus.OK); t.is(res.status, HttpStatus.OK);
t.is(res.get('publish-mode'), 'edgeless'); t.is(res.get('publish-mode'), 'edgeless');

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'node:crypto';
import { User, Workspace } from '@prisma/client'; import { User, Workspace } from '@prisma/client';
import ava, { TestFn } from 'ava'; import ava, { TestFn } from 'ava';
import request from 'supertest';
import { Doc as YDoc } from 'yjs'; import { Doc as YDoc } from 'yjs';
import { createTestingApp, type TestingApp } from '../../../__tests__/utils'; import { createTestingApp, type TestingApp } from '../../../__tests__/utils';
@@ -22,7 +21,7 @@ const test = ava as TestFn<{
}>; }>;
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
flavor: { flavor: {
@@ -47,7 +46,7 @@ let user: User;
let workspace: Workspace; let workspace: Workspace;
test.beforeEach(async t => { test.beforeEach(async t => {
t.context.config.docService.endpoint = t.context.app.getHttpServerUrl(); t.context.config.docService.endpoint = t.context.app.url();
await t.context.app.initTestingDB(); await t.context.app.initTestingDB();
user = await t.context.models.user.create({ user = await t.context.models.user.create({
email: 'test@affine.pro', email: 'test@affine.pro',
@@ -59,7 +58,7 @@ test.after.always(async t => {
await t.context.app.close(); await t.context.app.close();
}); });
test('should render page success', async t => { test.only('should render page success', async t => {
const docId = randomUUID(); const docId = randomUUID();
const { app, adapter, permission } = t.context; const { app, adapter, permission } = t.context;
@@ -78,8 +77,6 @@ test('should render page success', async t => {
await adapter.pushDocUpdates(workspace.id, docId, updates, user.id); await adapter.pushDocUpdates(workspace.id, docId, updates, user.id);
await permission.publishPage(workspace.id, docId); await permission.publishPage(workspace.id, docId);
await request(app.getHttpServer()) await app.GET(`/workspace/${workspace.id}/${docId}`).expect(200);
.get(`/workspace/${workspace.id}/${docId}`)
.expect(200);
t.pass(); t.pass();
}); });

View File

@@ -21,7 +21,7 @@ const test = ava as TestFn<{
}>; }>;
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
flavor: { flavor: {
@@ -46,7 +46,7 @@ let user: User;
let workspace: Workspace; let workspace: Workspace;
test.beforeEach(async t => { test.beforeEach(async t => {
t.context.config.docService.endpoint = t.context.app.getHttpServerUrl(); t.context.config.docService.endpoint = t.context.app.url();
await t.context.app.initTestingDB(); await t.context.app.initTestingDB();
user = await t.context.models.user.create({ user = await t.context.models.user.create({
email: 'test@affine.pro', email: 'test@affine.pro',

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'node:crypto';
import { User, Workspace } from '@prisma/client'; import { User, Workspace } from '@prisma/client';
import ava, { TestFn } from 'ava'; import ava, { TestFn } from 'ava';
import request from 'supertest';
import { createTestingApp, type TestingApp } from '../../../__tests__/utils'; import { createTestingApp, type TestingApp } from '../../../__tests__/utils';
import { AppModule } from '../../../app.module'; import { AppModule } from '../../../app.module';
@@ -17,7 +16,7 @@ const test = ava as TestFn<{
}>; }>;
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [ConfigModule.forRoot(), AppModule], imports: [ConfigModule.forRoot(), AppModule],
}); });
@@ -44,8 +43,8 @@ test.after.always(async t => {
test('should forbid access to rpc api without access token', async t => { test('should forbid access to rpc api without access token', async t => {
const { app } = t.context; const { app } = t.context;
await request(app.getHttpServer()) await app
.get('/rpc/workspaces/123/docs/123') .GET('/rpc/workspaces/123/docs/123')
.expect({ .expect({
status: 403, status: 403,
code: 'Forbidden', code: 'Forbidden',
@@ -60,8 +59,8 @@ test('should forbid access to rpc api without access token', async t => {
test('should forbid access to rpc api with invalid access token', async t => { test('should forbid access to rpc api with invalid access token', async t => {
const { app } = t.context; const { app } = t.context;
await request(app.getHttpServer()) await app
.get('/rpc/workspaces/123/docs/123') .GET('/rpc/workspaces/123/docs/123')
.set('x-access-token', 'invalid,wrong-signature') .set('x-access-token', 'invalid,wrong-signature')
.expect({ .expect({
status: 403, status: 403,
@@ -79,8 +78,8 @@ test('should 404 when doc not found', async t => {
const workspaceId = '123'; const workspaceId = '123';
const docId = '123'; const docId = '123';
await request(app.getHttpServer()) await app
.get(`/rpc/workspaces/${workspaceId}/docs/${docId}`) .GET(`/rpc/workspaces/${workspaceId}/docs/${docId}`)
.set('x-access-token', t.context.crypto.sign(docId)) .set('x-access-token', t.context.crypto.sign(docId))
.expect({ .expect({
status: 404, status: 404,
@@ -108,8 +107,8 @@ test('should return doc when found', async t => {
}, },
]); ]);
const res = await request(app.getHttpServer()) const res = await app
.get(`/rpc/workspaces/${workspace.id}/docs/${docId}`) .GET(`/rpc/workspaces/${workspace.id}/docs/${docId}`)
.set('x-access-token', t.context.crypto.sign(docId)) .set('x-access-token', t.context.crypto.sign(docId))
.set('x-rpc-trace-id', 'test-trace-id') .set('x-rpc-trace-id', 'test-trace-id')
.expect(200) .expect(200)

View File

@@ -17,7 +17,7 @@ const test = ava as TestFn<{
}>; }>;
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [ConfigModule.forRoot(), AppModule], imports: [ConfigModule.forRoot(), AppModule],
}); });

View File

@@ -20,7 +20,7 @@ const test = ava as TestFn<{
}>; }>;
test.before(async t => { test.before(async t => {
const { app } = await createTestingApp({ const app = await createTestingApp({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
flavor: { flavor: {
@@ -44,7 +44,7 @@ let user: User;
let workspace: Workspace; let workspace: Workspace;
test.beforeEach(async t => { test.beforeEach(async t => {
t.context.config.docService.endpoint = t.context.app.getHttpServerUrl(); t.context.config.docService.endpoint = t.context.app.url();
await t.context.app.initTestingDB(); await t.context.app.initTestingDB();
user = await t.context.models.user.create({ user = await t.context.models.user.create({
email: 'test@affine.pro', email: 'test@affine.pro',

View File

@@ -17,6 +17,7 @@ export class PromptService implements OnModuleInit {
constructor(private readonly db: PrismaClient) {} constructor(private readonly db: PrismaClient) {}
async onModuleInit() { async onModuleInit() {
this.cache.clear();
await refreshPrompts(this.db); await refreshPrompts(this.db);
} }