mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
test(server): utils (#10028)
This commit is contained in:
@@ -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()],
|
||||
});
|
||||
|
||||
|
||||
@@ -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()],
|
||||
});
|
||||
|
||||
|
||||
@@ -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()],
|
||||
});
|
||||
|
||||
|
||||
@@ -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()],
|
||||
});
|
||||
|
||||
|
||||
@@ -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()],
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/// <reference types="../global.d.ts" />
|
||||
|
||||
import type { ExecutionContext, TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}.`,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
243
packages/backend/server/src/__tests__/utils/testing-app.ts
Normal file
243
packages/backend/server/src/__tests__/utils/testing-app.ts
Normal 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];
|
||||
},
|
||||
});
|
||||
}
|
||||
110
packages/backend/server/src/__tests__/utils/testing-module.ts
Normal file
110
packages/backend/server/src/__tests__/utils/testing-module.ts
Normal 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;
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user