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,
doc: true,
} satisfies typeof AFFiNE.flavor;
const { app } = await createTestingApp({
const app = await createTestingApp({
imports: [buildAppModule()],
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,12 +4,10 @@ import {
getCurrentMailMessageCount,
getTokenFromLatestMailMessage,
} from '@affine-test/kit/utils/cloud';
import type { INestApplication } from '@nestjs/common';
import type { TestFn } from 'ava';
import ava from 'ava';
import { MailService } from '../../base/mailer';
import { AuthService } from '../../core/auth/service';
import {
changeEmail,
changePassword,
@@ -18,21 +16,18 @@ import {
sendChangeEmail,
sendSetPasswordEmail,
sendVerifyChangeEmail,
signUp,
TestingApp,
} from '../utils';
const test = ava as TestFn<{
app: INestApplication;
auth: AuthService;
app: TestingApp;
mail: MailService;
}>;
test.beforeEach(async t => {
const { app } = await createTestingApp();
const auth = app.get(AuthService);
const app = await createTestingApp();
const mail = app.get(MailService);
t.context.app = app;
t.context.auth = auth;
t.context.mail = mail;
});
@@ -46,11 +41,9 @@ test('change email', async t => {
const u1Email = 'u1@affine.pro';
const u2Email = 'u2@affine.pro';
const u1 = await signUp(app, 'u1', u1Email, '1');
await app.signup(u1Email);
const primitiveMailCount = await getCurrentMailMessageCount();
await sendChangeEmail(app, u1.token.token, u1Email, 'affine.pro');
await sendChangeEmail(app, u1Email, 'affine.pro');
const afterSendChangeMailCount = await getCurrentMailMessageCount();
t.is(
@@ -69,7 +62,6 @@ test('change email', async t => {
await sendVerifyChangeEmail(
app,
u1.token.token,
changeEmailToken as string,
u2Email,
'affine.pro'
@@ -91,7 +83,7 @@ test('change email', async t => {
'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();
@@ -105,15 +97,15 @@ test('change email', async t => {
});
test('set and change password', async t => {
const { mail, app, auth } = t.context;
const { mail, app } = t.context;
if (mail.hasConfigured()) {
const u1Email = 'u1@affine.pro';
const u1 = await signUp(app, 'u1', u1Email, '1');
const u1 = await app.signup(u1Email);
const primitiveMailCount = await getCurrentMailMessageCount();
await sendSetPasswordEmail(app, u1.token.token, u1Email, 'affine.pro');
await sendSetPasswordEmail(app, u1Email, 'affine.pro');
const afterSendSetMailCount = await getCurrentMailMessageCount();
@@ -141,78 +133,84 @@ test('set and change password', async t => {
t.true(success, 'failed to change password');
const ret = auth.signIn(u1Email, newPassword);
t.notThrowsAsync(ret, 'failed to check password');
t.is((await ret).id, u1.id, 'failed to check password');
await app.login({
...u1,
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();
});
test('should revoke token after change user identify', async t => {
const { mail, app, auth } = t.context;
const { mail, app } = t.context;
if (mail.hasConfigured()) {
// change email
{
const u1Email = 'u1@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');
}
await sendChangeEmail(app, u1.token.token, u1Email, 'affine.pro');
await sendChangeEmail(app, u1Email, 'affine.pro');
const changeEmailToken = await getTokenFromLatestMailMessage();
await sendVerifyChangeEmail(
app,
u1.token.token,
changeEmailToken as string,
u2Email,
'affine.pro'
);
const verifyEmailToken = await getTokenFromLatestMailMessage();
await changeEmail(
app,
u1.token.token,
verifyEmailToken as string,
u2Email
);
await changeEmail(app, 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');
const newUserSession = await auth.signIn(u2Email, '1');
t.is(newUserSession?.email, u2Email, 'failed to sign in with new email');
await app.login({
...u1,
email: u2Email,
});
user = await currentUser(app);
t.is(user?.email, u2Email, 'failed to sign in with new email');
}
// change password
{
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');
}
await sendSetPasswordEmail(app, u3.token.token, u3Email, 'affine.pro');
await sendSetPasswordEmail(app, u3Email, 'affine.pro');
const token = await getTokenFromLatestMailMessage();
const newPassword = randomBytes(16).toString('hex');
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');
const newUserSession = await auth.signIn(u3Email, newPassword);
t.is(
newUserSession?.email,
u3Email,
'failed to sign in with new password'
);
await app.login({
...u3,
password: newPassword,
});
user = await currentUser(app);
t.is(user?.email, u3Email, 'failed to sign in with new password');
}
}
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 ava, { TestFn } from 'ava';
import Sinon from 'sinon';
import request from 'supertest';
import { MailService } from '../../base';
import { AuthModule, CurrentUser } from '../../core/auth';
import { AuthModule } from '../../core/auth';
import { AuthService } from '../../core/auth/service';
import { FeatureModule } from '../../core/features';
import { UserModule } from '../../core/user';
import { createTestingApp, getSession, sessionCookie } from '../utils';
import {
createTestingApp,
currentUser,
parseCookies,
TestingApp,
} from '../utils';
const test = ava as TestFn<{
auth: AuthService;
u1: CurrentUser;
db: PrismaClient;
mailer: Sinon.SinonStubbedInstance<MailService>;
app: INestApplication;
app: TestingApp;
}>;
test.before(async t => {
const { app } = await createTestingApp({
const app = await createTestingApp({
imports: [FeatureModule, UserModule, AuthModule],
tapModule: m => {
m.overrideProvider(MailService).useValue(
@@ -36,12 +39,11 @@ test.before(async t => {
t.context.db = app.get(PrismaClient);
t.context.mailer = app.get(MailService);
t.context.app = app;
t.context.u1 = await t.context.auth.signUp('u1@affine.pro', '1');
});
test.beforeEach(() => {
test.beforeEach(async t => {
Sinon.reset();
await t.context.app.initTestingDB();
});
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 => {
const { app, u1 } = t.context;
const { app } = t.context;
const res = await request(app.getHttpServer())
.post('/api/auth/sign-in')
.send({ email: u1.email, password: '1' })
const u1 = await app.createUser('u1@affine.pro');
await app
.POST('/api/auth/sign-in')
.send({ email: u1.email, password: u1.password })
.expect(200);
const session = await getSession(app, res);
t.is(session.user!.id, u1.id);
const session = await currentUser(app);
t.is(session?.id, u1.id);
});
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
mailer.sendSignInMail.resolves({ rejected: [] });
const res = await request(app.getHttpServer())
.post('/api/auth/sign-in')
const res = await app
.POST('/api/auth/sign-in')
.send({ email: u1.email })
.expect(200);
@@ -79,13 +84,10 @@ test('should be able to sign in with email', async t => {
const email = url.searchParams.get('email');
const token = url.searchParams.get('token');
const signInRes = await request(app.getHttpServer())
.post('/api/auth/magic-link')
.send({ email, token })
.expect(201);
await app.POST('/api/auth/magic-link').send({ email, token }).expect(201);
const session = await getSession(app, signInRes);
t.is(session.user!.id, u1.id);
const session = await currentUser(app);
t.is(session?.id, u1.id);
});
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
mailer.sendSignUpMail.resolves({ rejected: [] });
const res = await request(app.getHttpServer())
.post('/api/auth/sign-in')
const res = await app
.POST('/api/auth/sign-in')
.send({ email: 'u2@affine.pro' })
.expect(200);
@@ -107,20 +109,17 @@ test('should be able to sign up with email', async t => {
const email = url.searchParams.get('email');
const token = url.searchParams.get('token');
const signInRes = await request(app.getHttpServer())
.post('/api/auth/magic-link')
.send({ email, token })
.expect(201);
await app.POST('/api/auth/magic-link').send({ email, token }).expect(201);
const session = await getSession(app, signInRes);
t.is(session.user!.email, 'u2@affine.pro');
const session = await currentUser(app);
t.is(session?.email, 'u2@affine.pro');
});
test('should not be able to sign in if email is invalid', async t => {
const { app } = t.context;
const res = await request(app.getHttpServer())
.post('/api/auth/sign-in')
const res = await app
.POST('/api/auth/sign-in')
.send({ email: '' })
.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 => {
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);
await request(app.getHttpServer())
.post('/api/auth/sign-in')
await app
.POST('/api/auth/sign-in')
.send({ email: u1.email })
.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 => {
const { app, u1 } = t.context;
const { app } = t.context;
const signInRes = await request(app.getHttpServer())
.post('/api/auth/sign-in')
.send({ email: u1.email, password: '1' })
const u1 = await app.createUser('u1@affine.pro');
await app
.POST('/api/auth/sign-in')
.send({ email: u1.email, password: u1.password })
.expect(200);
const cookie = sessionCookie(signInRes.headers);
await app.GET('/api/auth/sign-out').expect(200);
await request(app.getHttpServer())
.get('/api/auth/sign-out')
.set('cookie', cookie)
.expect(200);
const session = await currentUser(app);
const session = await getSession(app, signInRes);
t.falsy(session.user);
t.falsy(session);
});
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())
.post('/api/auth/sign-in')
.send({ email: u1.email, password: '1' })
.expect(200);
const u1 = await app.signup('u1@affine.pro');
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())
.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);
t.is(userIdCookie, u1.id);
});
// multiple accounts session tests
test('should be able to sign in another account in one session', async t => {
const { app, 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
const signInRes = await request(app.getHttpServer())
.post('/api/auth/sign-in')
.send({ email: u1.email, password: '1' })
const res = await app
.POST('/api/auth/sign-in')
.send({ email: u1.email, password: u1.password })
.expect(200);
const cookie = sessionCookie(signInRes.headers);
// avoid create session at the exact same time, leads to same random session users order
await new Promise(resolve => setTimeout(resolve, 1));
const cookies = parseCookies(res);
// sign in u2 in the same session
await request(app.getHttpServer())
.post('/api/auth/sign-in')
.set('cookie', cookie)
.send({ email: u2.email, password: '3' })
await app
.POST('/api/auth/sign-in')
.send({ email: u2.email, password: u2.password })
.expect(200);
// list [u1, u2]
const sessions = await request(app.getHttpServer())
.get('/api/auth/sessions')
.set('cookie', cookie)
.expect(200);
const sessions = await app.GET('/api/auth/sessions').expect(200);
t.is(sessions.body.users.length, 2);
t.is(sessions.body.users[0].id, u1.id);
t.is(sessions.body.users[1].id, u2.id);
t.like(
sessions.body.users.map((u: any) => u.id),
[u1.id, u2.id]
);
// default to latest signed in user: u2
let session = await request(app.getHttpServer())
.get('/api/auth/session')
.set('cookie', cookie)
.expect(200);
let session = await app.GET('/api/auth/session').expect(200);
t.is(session.body.user.id, u2.id);
// switch to u1
session = await request(app.getHttpServer())
.get('/api/auth/session')
.set('cookie', `${cookie};${AuthService.userCookieName}=${u1.id}`)
session = await app
.GET('/api/auth/session')
.set(
'Cookie',
Object.entries(cookies)
.map(([k, v]) => `${k}=${v}`)
.join('; ')
)
.expect(200);
t.is(session.body.user.id, u1.id);
});
test('should be able to sign out multiple accounts in one session', async t => {
const { app, u1, auth } = t.context;
const { app } = t.context;
const u2 = await auth.signUp('u4@affine.pro', '4');
// 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);
const u1 = await app.signup('u1@affine.pro');
const u2 = await app.signup('u2@affine.pro');
// sign out u2
let signOut = await request(app.getHttpServer())
.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}`));
await app.GET(`/api/auth/sign-out?user_id=${u2.id}`).expect(200);
// list [u1]
const session = await request(app.getHttpServer())
.get('/api/auth/session')
.set('cookie', cookie)
.expect(200);
let session = await app.GET('/api/auth/session').expect(200);
t.is(session.body.user.id, u1.id);
// sign in u2 in the same session
await request(app.getHttpServer())
.post('/api/auth/sign-in')
.set('cookie', cookie)
.send({ email: u2.email, password: '4' })
await app
.POST('/api/auth/sign-in')
.send({ email: u2.email, password: u2.password })
.expect(200);
// sign out all account in session
signOut = await request(app.getHttpServer())
.get('/api/auth/sign-out')
.set('cookie', cookie)
.expect(200);
await app.GET('/api/auth/sign-out').expect(200);
t.true(
signOut
.get('Set-Cookie')
?.some(c => c.startsWith(`${AuthService.sessionCookieName}=;`))
);
t.true(
signOut
.get('Set-Cookie')
?.some(c => c.startsWith(`${AuthService.userCookieName}=;`))
);
session = await app.GET('/api/auth/session').expect(200);
t.falsy(session.body.user);
});

View File

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

View File

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

View File

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

View File

@@ -55,8 +55,9 @@ const test = ava as TestFn<{
json: CopilotCheckJsonExecutor;
};
}>;
let userId: string;
test.beforeEach(async t => {
test.before(async t => {
const module = await createTestingModule({
imports: [
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 => {
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');
userId = user.id;
});
test.after.always(async t => {
await t.context.module.close();
});
// ==================== prompt ====================
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 type { INestApplication } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import type { TestFn } from 'ava';
import ava from 'ava';
import request from 'supertest';
@@ -13,7 +12,6 @@ import { createTestingApp } from '../utils';
const test = ava as TestFn<{
app: INestApplication;
db: PrismaClient;
}>;
const mobileUAString =
@@ -51,12 +49,11 @@ test.before('init selfhost server', async t => {
const staticPath = new Package('@affine/server').join('static').value;
initTestStaticFiles(staticPath);
const { app } = await createTestingApp({
const app = await createTestingApp({
imports: [DocRendererModule],
});
t.context.app = app;
t.context.db = t.context.app.get(PrismaClient);
});
test.after.always(async t => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
import test from 'ava';
import request from 'supertest';
import { AppModule } from '../app.module';
import { createTestingApp, currentUser, signUp, TestingApp } from './utils';
import {
createTestingApp,
currentUser,
deleteAccount,
TestingApp,
} from './utils';
let app: TestingApp;
test.before(async () => {
const { app: testApp } = await createTestingApp({
imports: [AppModule],
});
app = testApp;
app = await createTestingApp();
});
test.beforeEach(async () => {
@@ -21,16 +21,12 @@ test.after.always(async () => {
await app.close();
});
test('should register a user', async t => {
const user = await signUp(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');
});
// TODO(@forehalo): signup test case
test.skip('should register a user', () => {});
test('should get current user', async t => {
const user = await signUp(app, 'u1', 'u1@affine.pro', '123456');
const currUser = await currentUser(app, user.token.token);
const user = await app.signup('u1@affine.pro');
const currUser = await currentUser(app);
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.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 => {
const user = await signUp(app, 'u1', 'u1@affine.pro', '123456');
await request(app.getHttpServer())
.post('/graphql')
.auth(user.token.token, { type: 'bearer' })
.send({
query: `
mutation {
deleteAccount {
success
}
}
`,
})
.expect(200);
t.is(await currentUser(app, user.token.token), null);
t.pass();
await app.signup('u1@affine.pro');
const deleted = await deleteAccount(app);
t.true(deleted);
const currUser = await currentUser(app);
t.is(currUser, null);
});

View File

@@ -1,71 +1,37 @@
import type { INestApplication } from '@nestjs/common';
import type { TestFn } from 'ava';
import ava from 'ava';
import request from 'supertest';
import { AppModule } from '../../app.module';
import { AuthService, CurrentUser } from '../../core/auth';
import { createTestingApp, gql, internalSignIn } from '../utils';
import { createTestingApp, TestingApp, updateAvatar } from '../utils';
const test = ava as TestFn<{
app: INestApplication;
u1: CurrentUser;
app: TestingApp;
}>;
test.beforeEach(async t => {
const { app } = await createTestingApp({
imports: [AppModule],
});
t.context.u1 = await app.get(AuthService).signUp('u1@affine.pro', '1');
test.before(async t => {
const app = await createTestingApp();
t.context.app = app;
});
test.afterEach.always(async t => {
await t.context.app.close();
test.beforeEach(async t => {
await t.context.app.initTestingDB();
});
async function fakeUploadAvatar(
app: INestApplication,
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.after.always(async t => {
await t.context.app.close();
});
test('should be able to upload user avatar', async t => {
const { app } = t.context;
await app.signup('u1@affine.pro');
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);
const avatarUrl = res.body.data.uploadAvatar.avatarUrl;
t.truthy(avatarUrl);
const avatarRes = await request(app.getHttpServer())
.get(new URL(avatarUrl).pathname)
.expect(200);
const avatarRes = await app.GET(new URL(avatarUrl).pathname);
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 => {
const { app } = t.context;
await app.signup('u1@affine.pro');
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 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;
t.not(oldAvatarUrl, newAvatarUrl);
await request(app.getHttpServer())
.get(new URL(oldAvatarUrl).pathname)
.expect(404);
const avatarRes = await app.GET(new URL(oldAvatarUrl).pathname);
t.is(avatarRes.status, 404);
const avatarRes = await request(app.getHttpServer())
.get(new URL(newAvatarUrl).pathname)
.expect(200);
t.deepEqual(avatarRes.body, Buffer.from('new'));
const newAvatarRes = await app.GET(new URL(newAvatarUrl).pathname);
t.deepEqual(newAvatarRes.body, Buffer.from('new'));
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,116 +1,84 @@
import type { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { DocRole } from '../../core/permission/types';
import { gql } from './common';
import { TestingApp } from './testing-app';
export function grantDocUserRoles(
app: INestApplication,
token: string,
export async function grantDocUserRoles(
app: TestingApp,
workspaceId: string,
docId: string,
userIds: string[],
role: DocRole
) {
return request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
grantDocUserRoles(input: {
workspaceId: "${workspaceId}",
docId: "${docId}",
userIds: ["${userIds.join('","')}"],
role: ${DocRole[role]}
})
}
`,
});
return await app.gql(`
mutation {
grantDocUserRoles(input: {
workspaceId: "${workspaceId}",
docId: "${docId}",
userIds: ["${userIds.join('","')}"],
role: ${DocRole[role]}
})
}
`);
}
export function revokeDocUserRoles(
app: INestApplication,
token: string,
export async function revokeDocUserRoles(
app: TestingApp,
workspaceId: string,
docId: string,
userId: string
) {
return request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
revokeDocUserRoles(input: {
workspaceId: "${workspaceId}",
docId: "${docId}",
userId: "${userId}"
})
}
`,
});
return await app.gql(`
mutation {
revokeDocUserRoles(input: {
workspaceId: "${workspaceId}",
docId: "${docId}",
userId: "${userId}"
})
}
`);
}
export function updateDocDefaultRole(
app: INestApplication,
token: string,
export async function updateDocDefaultRole(
app: TestingApp,
workspaceId: string,
docId: string,
role: DocRole
) {
return request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
updateDocDefaultRole(input: {
workspaceId: "${workspaceId}",
docId: "${docId}",
role: ${DocRole[role]}
})
}
`,
});
return await app.gql(`
mutation {
updateDocDefaultRole(input: {
workspaceId: "${workspaceId}",
docId: "${docId}",
role: ${DocRole[role]}
})
}
`);
}
export async function docGrantedUsersList(
app: INestApplication,
token: string,
app: TestingApp,
workspaceId: string,
docId: string,
first = 10,
offset = 0
) {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
query {
workspace(id: "${workspaceId}") {
doc(docId: "${docId}") {
grantedUsersList(pagination: { first: ${first}, offset: ${offset} }) {
totalCount
edges {
cursor
node {
role
user {
id
}
}
return await app.gql(`
query {
workspace(id: "${workspaceId}") {
doc(docId: "${docId}") {
grantedUsersList(pagination: { first: ${first}, offset: ${offset} }) {
totalCount
edges {
cursor
node {
role
user {
id
}
}
}
}
}
`,
});
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,201 +1,124 @@
import type { INestApplication } from '@nestjs/common';
import request, { type Response } from 'supertest';
import { TestingApp } from './testing-app';
import {
AuthService,
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 {
currentUser {
id, name, email, emailVerified, avatarUrl, hasPassword,
token { token }
}
}
`,
})
.expect(200);
return res.body.data.currentUser;
export async function currentUser(app: TestingApp) {
const res = await app.gql(`
query {
currentUser {
id, name, email, emailVerified, avatarUrl, hasPassword,
token { token }
}
}
`);
return res.currentUser;
}
export async function sendChangeEmail(
app: INestApplication,
userToken: string,
app: TestingApp,
email: string,
callbackUrl: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
sendChangeEmail(email: "${email}", callbackUrl: "${callbackUrl}")
}
`,
})
.expect(200);
const res = await app.gql(`
mutation {
sendChangeEmail(email: "${email}", callbackUrl: "${callbackUrl}")
}
`);
return res.body.data.sendChangeEmail;
return res.sendChangeEmail;
}
export async function sendSetPasswordEmail(
app: INestApplication,
userToken: string,
app: TestingApp,
email: string,
callbackUrl: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
sendSetPasswordEmail(email: "${email}", callbackUrl: "${callbackUrl}")
}
`,
})
.expect(200);
const res = await app.gql(`
mutation {
sendSetPasswordEmail(email: "${email}", callbackUrl: "${callbackUrl}")
}
`);
return res.body.data.sendChangeEmail;
return res.sendSetPasswordEmail;
}
export async function changePassword(
app: INestApplication,
app: TestingApp,
userId: string,
token: string,
password: string
): Promise<string> {
const res = await request(app.getHttpServer())
.post(gql)
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation changePassword($token: String!, $userId: String!, $password: String!) {
changePassword(token: $token, userId: $userId, newPassword: $password)
}
`,
variables: { token, password, userId },
})
.expect(200);
return res.body.data.changePassword;
const res = await app.gql(`
mutation {
changePassword(token: "${token}", userId: "${userId}", newPassword: "${password}")
}
`);
return res.changePassword;
}
export async function sendVerifyChangeEmail(
app: INestApplication,
userToken: string,
app: TestingApp,
token: string,
email: string,
callbackUrl: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
sendVerifyChangeEmail(token:"${token}", email: "${email}", callbackUrl: "${callbackUrl}")
}
`,
})
.expect(200);
const res = await app.gql(`
mutation {
sendVerifyChangeEmail(token: "${token}", email: "${email}", callbackUrl: "${callbackUrl}")
}
`);
return res.body.data.sendVerifyChangeEmail;
return res.sendVerifyChangeEmail;
}
export async function changeEmail(
app: INestApplication,
userToken: string,
app: TestingApp,
token: string,
email: string
): Promise<UserAuthedType> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
changeEmail(token: "${token}", email: "${email}") {
id
name
avatarUrl
email
}
}
`,
})
.expect(200);
return res.body.data.changeEmail;
) {
const res = await app.gql(`
mutation {
changeEmail(token: "${token}", email: "${email}") {
id
name
avatarUrl
email
}
}
`);
return res.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 { APP_GUARD, ModuleRef } from '@nestjs/core';
import { Query, Resolver } from '@nestjs/graphql';
import {
Test,
TestingModule as BaseTestingModule,
TestingModuleBuilder,
} from '@nestjs/testing';
import { INestApplicationContext, LogLevel } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
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 { ModelsModule } from '../../models';
const TEST_LOG_LEVEL: LogLevel =
export const TEST_LOG_LEVEL: LogLevel =
(process.env.TEST_LOG_LEVEL as LogLevel) ?? 'fatal';
async function flushDB(client: PrismaClient) {
@@ -38,190 +23,10 @@ async function flushDB(client: PrismaClient) {
);
}
interface TestingModuleMetadata extends ModuleMetadata {
tapModule?(m: TestingModuleBuilder): void;
tapApp?(app: INestApplication): void;
}
const initTestingDB = async (ref: ModuleRef) => {
const db = ref.get(PrismaClient, { strict: false });
export async function initTestingDB(context: INestApplicationContext) {
const db = context.get(PrismaClient, { strict: false });
await flushDB(db);
await RefreshFeatures0001.up(db, ref);
};
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;
await RefreshFeatures0001.up(db, context.get(ModuleRef));
}
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 type { WorkspaceType } from '../../core/workspaces';
import { gql } from './common';
import { TestingApp } from './testing-app';
export async function createWorkspace(
app: INestApplication,
token: string
): Promise<WorkspaceType> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
export async function createWorkspace(app: TestingApp): Promise<WorkspaceType> {
const res = await app
.POST('/graphql')
.set({
'x-request-id': 'test',
'x-operation-name': 'test',
})
.field(
'operations',
JSON.stringify({
@@ -26,181 +22,141 @@ export async function createWorkspace(
})
)
.field('map', JSON.stringify({ '0': ['variables.init'] }))
.attach('0', Buffer.from([0, 0]), 'init.data')
.expect(200);
.attach('0', Buffer.from([0, 0]), 'init.data');
return res.body.data.createWorkspace;
}
export async function getWorkspacePublicPages(
app: INestApplication,
token: string,
export async function getWorkspacePublicDocs(
app: TestingApp,
workspaceId: 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 {
workspace(id: "${workspaceId}") {
publicPages {
id
mode
}
}
}
`,
})
.expect(200);
return res.body.data.workspace.publicPages;
const res = await app.gql(
`
query {
workspace(id: "${workspaceId}") {
publicDocs {
id
mode
}
}
}
`
);
return res.workspace.publicDocs;
}
export async function getWorkspace(
app: INestApplication,
token: string,
app: TestingApp,
workspaceId: string,
skip = 0,
take = 8
): Promise<WorkspaceType> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
query {
workspace(id: "${workspaceId}") {
id, members(skip: ${skip}, take: ${take}) { id, name, email, permission, inviteId, status }
}
}
`,
})
.expect(200);
return res.body.data.workspace;
const res = await app.gql(
`
query {
workspace(id: "${workspaceId}") {
id,
members(skip: ${skip}, take: ${take}) { id, name, email, permission, inviteId, status }
}
}
`
);
return res.workspace;
}
export async function updateWorkspace(
app: INestApplication,
token: string,
app: TestingApp,
workspaceId: string,
isPublic: boolean
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
updateWorkspace(input: { id: "${workspaceId}", public: ${isPublic} }) {
public
}
}
`,
})
.expect(200);
return res.body.data.updateWorkspace.public;
const res = await app.gql(
`
mutation {
updateWorkspace(input: { id: "${workspaceId}", public: ${isPublic} }) {
public
}
}
`
);
return res.updateWorkspace.public;
}
export async function publishDoc(
app: INestApplication,
token: string,
app: TestingApp,
workspaceId: string,
docId: string
) {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
publishDoc(workspaceId: "${workspaceId}", docId: "${docId}") {
id
mode
}
}
`,
})
.expect(200);
return res.body.errors?.[0]?.message || res.body.data?.publishDoc;
const res = await app.gql(
`
mutation {
publishDoc(workspaceId: "${workspaceId}", docId: "${docId}") {
id
mode
}
}
`
);
return res.publishDoc;
}
export async function revokePublicDoc(
app: INestApplication,
token: string,
app: TestingApp,
workspaceId: string,
docId: string
) {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
revokePublicDoc(workspaceId: "${workspaceId}", docId: "${docId}") {
id
mode
public
}
}
`,
})
.expect(200);
return res.body.errors?.[0]?.message || res.body.data?.revokePublicDoc;
const res = await app.gql(
`
mutation {
revokePublicDoc(workspaceId: "${workspaceId}", docId: "${docId}") {
id
mode
public
}
}
`
);
return res.revokePublicDoc;
}
export async function grantMember(
app: INestApplication,
token: string,
app: TestingApp,
workspaceId: string,
userId: string,
permission: WorkspaceRole
) {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
grantMember(
workspaceId: "${workspaceId}"
userId: "${userId}"
permission: ${WorkspaceRole[permission]}
)
}
`,
})
.expect(200);
if (res.body.errors) {
throw new Error(res.body.errors[0].message);
}
return res.body.data?.grantMember;
const res = await app.gql(
`
mutation {
grantMember(
workspaceId: "${workspaceId}"
userId: "${userId}"
permission: ${WorkspaceRole[permission]}
)
}
`
);
return res.grantMember;
}
export async function revokeMember(
app: INestApplication,
token: string,
app: TestingApp,
workspaceId: string,
userId: string
) {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation {
revoke(workspaceId: "${workspaceId}", userId: "${userId}")
}
`,
})
.expect(200);
if (res.body.errors) {
throw new Error(res.body.errors[0].message);
}
return res.body.data?.revokeMember;
const res = await app.gql(
`
mutation {
revoke(workspaceId: "${workspaceId}", userId: "${userId}")
}
`
);
return res.revoke;
}

View File

@@ -6,7 +6,6 @@ import { PrismaClient } from '@prisma/client';
import type { TestFn } from 'ava';
import ava from 'ava';
import { AppModule } from '../app.module';
import { MailService } from '../base/mailer';
import { AuthService } from '../core/auth/service';
import { Models } from '../models';
@@ -18,7 +17,6 @@ import {
inviteUser,
leaveWorkspace,
revokeUser,
signUp,
TestingApp,
} from './utils';
@@ -31,9 +29,7 @@ const test = ava as TestFn<{
}>;
test.before(async t => {
const { app } = await createTestingApp({
imports: [AppModule],
});
const app = await createTestingApp();
t.context.app = app;
t.context.client = app.get(PrismaClient);
t.context.auth = app.get(AuthService);
@@ -51,52 +47,53 @@ test.after.always(async t => {
test('should invite a user', async t => {
const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
const u2 = await app.signup('u2@affine.pro');
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');
});
test('should leave a workspace', async t => {
const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
const u2 = await app.signup('u2@affine.pro');
await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token);
const id = await inviteUser(app, u1.token.token, workspace.id, u2.email);
await acceptInviteById(app, workspace.id, id, false);
const workspace = await createWorkspace(app);
const invite = await inviteUser(app, workspace.id, u2.email);
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');
});
test('should revoke a user', async t => {
const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
const u2 = await app.signup('u2@affine.pro');
await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token);
await inviteUser(app, u1.token.token, workspace.id, u2.email);
const workspace = await createWorkspace(app);
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');
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');
});
test('should create user if not exist', async t => {
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');
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 => {
const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
const u2 = await app.signup('u2@affine.pro');
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);
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');
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);
t.not(currMember, undefined, 'failed to invite user');
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 => {
const { mail, app } = t.context;
if (mail.hasConfigured()) {
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
const u2 = await signUp(app, 'test', 'production@toeverything.info', '1');
const u2 = await app.signup('u2@affine.pro');
await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token);
const workspace = await createWorkspace(app);
const primitiveMailCount = await getCurrentMailMessageCount();
const invite = await inviteUser(
app,
u1.token.token,
workspace.id,
u2.email,
true
);
const invite = await inviteUser(app, workspace.id, u2.email, true);
const afterInviteMailCount = await getCurrentMailMessageCount();
t.is(
@@ -152,12 +145,13 @@ test('should send email', async t => {
t.not(
inviteEmailContent.To.find((item: any) => {
return item.Mailbox === 'production';
return item.Mailbox === 'u2';
}),
undefined,
'invite email address was incorrectly sent'
);
app.switchUser(u2.id);
const accept = await acceptInviteById(app, workspace.id, invite, true);
t.true(accept, 'failed to accept invite');
@@ -176,7 +170,7 @@ test('should send email', async t => {
'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
// const afterLeaveMailCount = await getCurrentMailMessageCount();
@@ -199,44 +193,28 @@ test('should send email', async t => {
test('should support pagination for member', async t => {
const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
const u3 = await signUp(app, 'u3', 'u3@affine.pro', '1');
await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app, u1.token.token);
const invite1 = await inviteUser(app, u1.token.token, workspace.id, u2.email);
const invite2 = await inviteUser(app, u1.token.token, workspace.id, u3.email);
const workspace = await createWorkspace(app);
await inviteUser(app, workspace.id, 'u2@affine.pro');
await inviteUser(app, workspace.id, 'u3@affine.pro');
await acceptInviteById(app, workspace.id, invite1, false);
await acceptInviteById(app, workspace.id, invite2, false);
const firstPageWorkspace = await getWorkspace(
app,
u1.token.token,
workspace.id,
0,
2
);
const firstPageWorkspace = await getWorkspace(app, workspace.id, 0, 2);
t.is(firstPageWorkspace.members.length, 2, 'failed to check invite id');
const secondPageWorkspace = await getWorkspace(
app,
u1.token.token,
workspace.id,
2,
2
);
const secondPageWorkspace = await getWorkspace(app, workspace.id, 2, 2);
t.is(secondPageWorkspace.members.length, 1, 'failed to check invite id');
});
test('should limit member count correctly', async t => {
const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
const workspace = await createWorkspace(app, u1.token.token);
await app.signup('u1@affine.pro');
const workspace = await createWorkspace(app);
await Promise.allSettled(
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');
});

View File

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

View File

@@ -1,7 +1,5 @@
import test from 'ava';
import request from 'supertest';
import { AppModule } from '../../app.module';
import { WorkspaceFeatureModel } from '../../models';
import {
collectAllBlobSizes,
@@ -10,7 +8,6 @@ import {
getWorkspaceBlobsSize,
listBlobs,
setBlob,
signUp,
TestingApp,
} from '../utils';
@@ -27,11 +24,7 @@ let app: TestingApp;
let model: WorkspaceFeatureModel;
test.before(async () => {
const { app: testApp } = await createTestingApp({
imports: [AppModule],
});
app = testApp;
app = await createTestingApp();
model = app.get(WorkspaceFeatureModel);
});
@@ -44,117 +37,113 @@ test.after.always(async () => {
});
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 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 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 request(server)
.get(`/api/workspaces/${workspace.id}/blobs/${hash1}`)
.auth(u1.token.token, { type: 'bearer' })
const response1 = await app
.GET(`/api/workspaces/${workspace.id}/blobs/${hash1}`)
.buffer();
t.deepEqual(response1.body, buffer1, 'failed to get blob');
const response2 = await request(server)
.get(`/api/workspaces/${workspace.id}/blobs/${hash2}`)
.auth(u1.token.token, { type: 'bearer' })
const response2 = await app
.GET(`/api/workspaces/${workspace.id}/blobs/${hash2}`)
.buffer();
t.deepEqual(response2.body, buffer2, 'failed to get blob');
});
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 blobs = await listBlobs(app, u1.token.token, workspace.id);
const workspace = await createWorkspace(app);
const blobs = await listBlobs(app, workspace.id);
t.is(blobs.length, 0, 'failed to list blobs');
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 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');
// list blob result is not ordered
t.deepEqual(ret.sort(), [hash1, hash2].sort());
});
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]);
await setBlob(app, u1.token.token, workspace.id, buffer1);
await setBlob(app, workspace.id, buffer1);
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');
});
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]);
await setBlob(app, u1.token.token, workspace1.id, buffer1);
await setBlob(app, workspace1.id, buffer1);
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]);
await setBlob(app, u1.token.token, workspace2.id, buffer3);
await setBlob(app, workspace2.id, buffer3);
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');
});
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);
const buffer1 = Buffer.from(
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 => {
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);
const buffer = Buffer.from(Array.from({ length: OneMB }, () => 0));
await t.notThrowsAsync(setBlob(app, u1.token.token, workspace.id, buffer));
await t.throwsAsync(setBlob(app, u1.token.token, workspace.id, buffer));
await t.notThrowsAsync(setBlob(app, 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 => {
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, 'unlimited_workspace', 'test');
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, u1.token.token, workspace.id, buffer));
await t.notThrowsAsync(setBlob(app, workspace.id, buffer));
await t.notThrowsAsync(setBlob(app, workspace.id, buffer));
});

View File

@@ -1,29 +1,24 @@
import { Readable } from 'node:stream';
import { HttpStatus, INestApplication } from '@nestjs/common';
import { HttpStatus } from '@nestjs/common';
import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client';
import ava, { TestFn } from 'ava';
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 { WorkspaceBlobStorage } from '../../core/storage';
import { createTestingApp, internalSignIn } from '../utils';
import { createTestingApp, TestingApp, TestUser } from '../utils';
const test = ava as TestFn<{
u1: CurrentUser;
db: PrismaClient;
app: INestApplication;
app: TestingApp;
u1: TestUser;
storage: Sinon.SinonStubbedInstance<WorkspaceBlobStorage>;
workspace: Sinon.SinonStubbedInstance<PgWorkspaceDocStorageAdapter>;
}>;
test.before(async t => {
const { app } = await createTestingApp({
imports: [AppModule],
const app = await createTestingApp({
tapModule: m => {
m.overrideProvider(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);
t.context.u1 = await app.signup('u1@affine.pro');
t.context.db = db;
t.context.app = app;
t.context.storage = app.get(WorkspaceBlobStorage);
@@ -109,23 +103,19 @@ function blob() {
// blob
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
storage.get.resolves(blob());
let res = await request(t.context.app.getHttpServer()).get(
'/api/workspaces/public/blobs/test'
);
let res = await app.GET('/api/workspaces/public/blobs/test');
t.is(res.status, HttpStatus.OK);
t.is(res.get('content-type'), 'text/plain');
t.is(res.text, 'blob');
// authenticated user
const cookie = await internalSignIn(app, u1.id);
res = await request(t.context.app.getHttpServer())
.get('/api/workspaces/public/blobs/test')
.set('Cookie', cookie);
await app.login(t.context.u1);
res = await app.GET('/api/workspaces/public/blobs/test');
t.is(res.status, HttpStatus.OK);
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 => {
const { app, u1, storage } = t.context;
const { app, storage } = t.context;
// no authenticated user
storage.get.resolves(blob());
let res = await request(app.getHttpServer()).get(
'/api/workspaces/private/blobs/test'
);
let res = await app.GET('/api/workspaces/private/blobs/test');
t.is(res.status, HttpStatus.OK);
t.is(res.get('content-type'), 'text/plain');
t.is(res.text, 'blob');
// authenticated user
const cookie = await internalSignIn(app, u1.id);
res = await request(app.getHttpServer())
.get('/api/workspaces/private/blobs/test')
.set('cookie', cookie);
await app.login(t.context.u1);
res = await app.GET('/api/workspaces/private/blobs/test');
t.is(res.status, HttpStatus.OK);
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 => {
const { app, u1 } = t.context;
const { app } = t.context;
let res = await request(app.getHttpServer()).get(
'/api/workspaces/totally-private/blobs/test'
);
let res = await app.GET('/api/workspaces/totally-private/blobs/test');
t.is(res.status, HttpStatus.FORBIDDEN);
res = await request(app.getHttpServer())
.get('/api/workspaces/totally-private/blobs/test')
.set('cookie', await internalSignIn(app, u1.id));
res = await app.GET('/api/workspaces/totally-private/blobs/test');
t.is(res.status, HttpStatus.FORBIDDEN);
});
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({
data: {
workspaceId: 'totally-private',
userId: u1.id,
userId: t.context.u1.id,
type: 1,
accepted: true,
status: WorkspaceMemberStatus.Accepted,
@@ -187,9 +168,8 @@ test('should be able to get permission granted workspace', async t => {
});
storage.get.resolves(blob());
const res = await request(app.getHttpServer())
.get('/api/workspaces/totally-private/blobs/test')
.set('Cookie', cookie);
await app.login(t.context.u1);
const res = await app.GET('/api/workspaces/totally-private/blobs/test');
t.is(res.status, HttpStatus.OK);
t.is(res.text, 'blob');
@@ -200,9 +180,7 @@ test('should return 404 if blob not found', async t => {
// @ts-expect-error mock
storage.get.resolves({ body: null });
const res = await request(app.getHttpServer()).get(
'/api/workspaces/public/blobs/test'
);
const res = await app.GET('/api/workspaces/public/blobs/test');
t.is(res.status, HttpStatus.NOT_FOUND);
});
@@ -210,17 +188,14 @@ test('should return 404 if blob not found', async t => {
// doc
// 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 => {
const { app, u1 } = t.context;
const { app } = t.context;
let res = await request(app.getHttpServer()).get(
'/api/workspaces/private/docs/private-page'
);
let res = await app.GET('/api/workspaces/private/docs/private-page');
t.is(res.status, HttpStatus.FORBIDDEN);
res = await request(app.getHttpServer())
.get('/api/workspaces/private/docs/private-page')
.set('cookie', await internalSignIn(app, u1.id));
await app.login(t.context.u1);
res = await app.GET('/api/workspaces/private/docs/private-page');
t.is(res.status, HttpStatus.FORBIDDEN);
});
@@ -235,9 +210,7 @@ test('should be able to get doc', async t => {
timestamp: Date.now(),
});
const res = await request(app.getHttpServer()).get(
'/api/workspaces/private/docs/public'
);
const res = await app.GET('/api/workspaces/private/docs/public');
t.is(res.status, HttpStatus.OK);
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(),
});
let res = await request(app.getHttpServer()).get(
'/api/workspaces/private/docs/public'
);
let res = await app.GET('/api/workspaces/private/docs/public');
t.is(res.status, HttpStatus.OK);
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 },
});
res = await request(app.getHttpServer()).get(
'/api/workspaces/private/docs/public'
);
res = await app.GET('/api/workspaces/private/docs/public');
t.is(res.status, HttpStatus.OK);
t.is(res.get('publish-mode'), 'edgeless');