From c3f5b4abb4c8c5d312210e49798c5963f03f7e0d Mon Sep 17 00:00:00 2001 From: forehalo Date: Mon, 10 Feb 2025 06:35:11 +0000 Subject: [PATCH] test(server): utils (#10028) --- .../server/src/__tests__/app/doc.e2e.ts | 2 +- .../server/src/__tests__/app/graphql.e2e.ts | 2 +- .../server/src/__tests__/app/renderer.e2e.ts | 2 +- .../server/src/__tests__/app/selfhost.e2e.ts | 2 +- .../server/src/__tests__/app/sync.e2e.ts | 2 +- .../server/src/__tests__/auth/auth.e2e.ts | 86 ++-- .../src/__tests__/auth/controller.spec.ts | 249 ++++------ .../server/src/__tests__/auth/guard.spec.ts | 2 +- .../src/__tests__/copilot-provider.spec.ts | 2 - .../server/src/__tests__/copilot.e2e.ts | 270 +++++------ .../server/src/__tests__/copilot.spec.ts | 17 +- .../server/src/__tests__/doc/renderer.spec.ts | 5 +- .../server/src/__tests__/mailer.spec.ts | 27 +- .../__tests__/nestjs/error-handler.spec.ts | 2 +- .../src/__tests__/nestjs/throttler.spec.ts | 116 ++--- .../src/__tests__/oauth/controller.spec.ts | 70 ++- .../src/__tests__/payment/service.spec.ts | 2 +- .../backend/server/src/__tests__/team.e2e.ts | 457 +++++++----------- .../backend/server/src/__tests__/user.e2e.ts | 47 +- .../server/src/__tests__/user/user.e2e.ts | 75 +-- .../server/src/__tests__/utils/blobs.ts | 91 ++-- .../server/src/__tests__/utils/copilot.ts | 209 +++----- .../server/src/__tests__/utils/index.ts | 2 + .../server/src/__tests__/utils/invite.ts | 334 +++++-------- .../server/src/__tests__/utils/permission.ts | 134 ++--- .../server/src/__tests__/utils/testing-app.ts | 243 ++++++++++ .../src/__tests__/utils/testing-module.ts | 110 +++++ .../server/src/__tests__/utils/user.ts | 253 ++++------ .../server/src/__tests__/utils/utils.ts | 207 +------- .../server/src/__tests__/utils/workspace.ts | 248 ++++------ .../src/__tests__/workspace-invite.e2e.ts | 118 ++--- .../server/src/__tests__/workspace.e2e.ts | 194 +++----- .../src/__tests__/workspace/blobs.e2e.ts | 89 ++-- .../__tests__/workspace/controller.spec.ts | 89 ++-- .../doc-renderer/__tests__/controller.spec.ts | 11 +- .../doc-renderer/__tests__/service.spec.ts | 4 +- .../doc-service/__tests__/controller.spec.ts | 19 +- .../__tests__/reader-from-database.spec.ts | 2 +- .../doc/__tests__/reader-from-rpc.spec.ts | 4 +- .../src/plugins/copilot/prompt/service.ts | 1 + 40 files changed, 1577 insertions(+), 2222 deletions(-) create mode 100644 packages/backend/server/src/__tests__/utils/testing-app.ts create mode 100644 packages/backend/server/src/__tests__/utils/testing-module.ts diff --git a/packages/backend/server/src/__tests__/app/doc.e2e.ts b/packages/backend/server/src/__tests__/app/doc.e2e.ts index 127cb57c30..b63626a087 100644 --- a/packages/backend/server/src/__tests__/app/doc.e2e.ts +++ b/packages/backend/server/src/__tests__/app/doc.e2e.ts @@ -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()], }); diff --git a/packages/backend/server/src/__tests__/app/graphql.e2e.ts b/packages/backend/server/src/__tests__/app/graphql.e2e.ts index 573538374f..c22d208e15 100644 --- a/packages/backend/server/src/__tests__/app/graphql.e2e.ts +++ b/packages/backend/server/src/__tests__/app/graphql.e2e.ts @@ -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()], }); diff --git a/packages/backend/server/src/__tests__/app/renderer.e2e.ts b/packages/backend/server/src/__tests__/app/renderer.e2e.ts index 9a558b6a5b..d1edfb0fbe 100644 --- a/packages/backend/server/src/__tests__/app/renderer.e2e.ts +++ b/packages/backend/server/src/__tests__/app/renderer.e2e.ts @@ -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()], }); diff --git a/packages/backend/server/src/__tests__/app/selfhost.e2e.ts b/packages/backend/server/src/__tests__/app/selfhost.e2e.ts index 7533beeb4d..28142c86f2 100644 --- a/packages/backend/server/src/__tests__/app/selfhost.e2e.ts +++ b/packages/backend/server/src/__tests__/app/selfhost.e2e.ts @@ -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()], }); diff --git a/packages/backend/server/src/__tests__/app/sync.e2e.ts b/packages/backend/server/src/__tests__/app/sync.e2e.ts index 2a0413cd26..aa6a414b1e 100644 --- a/packages/backend/server/src/__tests__/app/sync.e2e.ts +++ b/packages/backend/server/src/__tests__/app/sync.e2e.ts @@ -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()], }); diff --git a/packages/backend/server/src/__tests__/auth/auth.e2e.ts b/packages/backend/server/src/__tests__/auth/auth.e2e.ts index 52d92cc7e3..8628b60267 100644 --- a/packages/backend/server/src/__tests__/auth/auth.e2e.ts +++ b/packages/backend/server/src/__tests__/auth/auth.e2e.ts @@ -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(); diff --git a/packages/backend/server/src/__tests__/auth/controller.spec.ts b/packages/backend/server/src/__tests__/auth/controller.spec.ts index 76253c2ddc..7e9eb6a007 100644 --- a/packages/backend/server/src/__tests__/auth/controller.spec.ts +++ b/packages/backend/server/src/__tests__/auth/controller.spec.ts @@ -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; - 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); }); diff --git a/packages/backend/server/src/__tests__/auth/guard.spec.ts b/packages/backend/server/src/__tests__/auth/guard.spec.ts index 57f96d3d9f..6a0ebd0500 100644 --- a/packages/backend/server/src/__tests__/auth/guard.spec.ts +++ b/packages/backend/server/src/__tests__/auth/guard.spec.ts @@ -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], }); diff --git a/packages/backend/server/src/__tests__/copilot-provider.spec.ts b/packages/backend/server/src/__tests__/copilot-provider.spec.ts index 72bdee3e1b..20c0c09b5e 100644 --- a/packages/backend/server/src/__tests__/copilot-provider.spec.ts +++ b/packages/backend/server/src/__tests__/copilot-provider.spec.ts @@ -1,5 +1,3 @@ -/// - import type { ExecutionContext, TestFn } from 'ava'; import ava from 'ava'; diff --git a/packages/backend/server/src/__tests__/copilot.e2e.ts b/packages/backend/server/src/__tests__/copilot.e2e.ts index 1f6e11ce17..fad4e36bde 100644 --- a/packages/backend/server/src/__tests__/copilot.e2e.ts +++ b/packages/backend/server/src/__tests__/copilot.e2e.ts @@ -1,5 +1,3 @@ -/// - 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'); }); diff --git a/packages/backend/server/src/__tests__/copilot.spec.ts b/packages/backend/server/src/__tests__/copilot.spec.ts index 7b9c5d2507..684c9a8435 100644 --- a/packages/backend/server/src/__tests__/copilot.spec.ts +++ b/packages/backend/server/src/__tests__/copilot.spec.ts @@ -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 => { diff --git a/packages/backend/server/src/__tests__/doc/renderer.spec.ts b/packages/backend/server/src/__tests__/doc/renderer.spec.ts index 6638f7b4b1..41710d6ffc 100644 --- a/packages/backend/server/src/__tests__/doc/renderer.spec.ts +++ b/packages/backend/server/src/__tests__/doc/renderer.spec.ts @@ -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 => { diff --git a/packages/backend/server/src/__tests__/mailer.spec.ts b/packages/backend/server/src/__tests__/mailer.spec.ts index 5d24495eab..f72097fed2 100644 --- a/packages/backend/server/src/__tests__/mailer.spec.ts +++ b/packages/backend/server/src/__tests__/mailer.spec.ts @@ -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); diff --git a/packages/backend/server/src/__tests__/nestjs/error-handler.spec.ts b/packages/backend/server/src/__tests__/nestjs/error-handler.spec.ts index a06dd1d313..d8d2ad52c6 100644 --- a/packages/backend/server/src/__tests__/nestjs/error-handler.spec.ts +++ b/packages/backend/server/src/__tests__/nestjs/error-handler.spec.ts @@ -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], }); diff --git a/packages/backend/server/src/__tests__/nestjs/throttler.spec.ts b/packages/backend/server/src/__tests__/nestjs/throttler.spec.ts index c139200067..97adc3f75c 100644 --- a/packages/backend/server/src/__tests__/nestjs/throttler.spec.ts +++ b/packages/backend/server/src/__tests__/nestjs/throttler.spec.ts @@ -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); diff --git a/packages/backend/server/src/__tests__/oauth/controller.spec.ts b/packages/backend/server/src/__tests__/oauth/controller.spec.ts index 5fa7668bbb..f9ebfd09db 100644 --- a/packages/backend/server/src/__tests__/oauth/controller.spec.ts +++ b/packages/backend/server/src/__tests__/oauth/controller.spec.ts @@ -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: { diff --git a/packages/backend/server/src/__tests__/payment/service.spec.ts b/packages/backend/server/src/__tests__/payment/service.spec.ts index 202060a891..673a001355 100644 --- a/packages/backend/server/src/__tests__/payment/service.spec.ts +++ b/packages/backend/server/src/__tests__/payment/service.spec.ts @@ -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: { diff --git a/packages/backend/server/src/__tests__/team.e2e.ts b/packages/backend/server/src/__tests__/team.e2e.ts index c8ee885582..fc7dcd2c15 100644 --- a/packages/backend/server/src/__tests__/team.e2e.ts +++ b/packages/backend/server/src/__tests__/team.e2e.ts @@ -1,14 +1,11 @@ -/// - 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 => { - 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}.`, + } + ); }); diff --git a/packages/backend/server/src/__tests__/user.e2e.ts b/packages/backend/server/src/__tests__/user.e2e.ts index 3b41c0ef17..f921cbd9b5 100644 --- a/packages/backend/server/src/__tests__/user.e2e.ts +++ b/packages/backend/server/src/__tests__/user.e2e.ts @@ -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); }); diff --git a/packages/backend/server/src/__tests__/user/user.e2e.ts b/packages/backend/server/src/__tests__/user/user.e2e.ts index ee6856eee1..3441d6c134 100644 --- a/packages/backend/server/src/__tests__/user/user.e2e.ts +++ b/packages/backend/server/src/__tests__/user/user.e2e.ts @@ -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')); }); diff --git a/packages/backend/server/src/__tests__/utils/blobs.ts b/packages/backend/server/src/__tests__/utils/blobs.ts index f6832c41c4..9e09a7f0e8 100644 --- a/packages/backend/server/src/__tests__/utils/blobs.ts +++ b/packages/backend/server/src/__tests__/utils/blobs.ts @@ -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 { - 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 { - 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 { - 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 { + 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 { - 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', diff --git a/packages/backend/server/src/__tests__/utils/copilot.ts b/packages/backend/server/src/__tests__/utils/copilot.ts index b78b256425..3b4d3f9224 100644 --- a/packages/backend/server/src/__tests__/utils/copilot.ts +++ b/packages/backend/server/src/__tests__/utils/copilot.ts @@ -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 { - 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 { - 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 { - 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 ): Promise { - 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 { 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 = {} ) { 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 { - 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 = { diff --git a/packages/backend/server/src/__tests__/utils/index.ts b/packages/backend/server/src/__tests__/utils/index.ts index 5ce5f44a9f..981cceee28 100644 --- a/packages/backend/server/src/__tests__/utils/index.ts +++ b/packages/backend/server/src/__tests__/utils/index.ts @@ -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'; diff --git a/packages/backend/server/src/__tests__/utils/invite.ts b/packages/backend/server/src/__tests__/utils/invite.ts index 83b0adfacb..91878968ec 100644 --- a/packages/backend/server/src/__tests__/utils/invite.ts +++ b/packages/backend/server/src/__tests__/utils/invite.ts @@ -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 { - 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> { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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; } diff --git a/packages/backend/server/src/__tests__/utils/permission.ts b/packages/backend/server/src/__tests__/utils/permission.ts index 06535f25b5..550c021c69 100644 --- a/packages/backend/server/src/__tests__/utils/permission.ts +++ b/packages/backend/server/src/__tests__/utils/permission.ts @@ -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; + } + } + `); } diff --git a/packages/backend/server/src/__tests__/utils/testing-app.ts b/packages/backend/server/src/__tests__/utils/testing-app.ts new file mode 100644 index 0000000000..fe2e0bca16 --- /dev/null +++ b/packages/backend/server/src/__tests__/utils/testing-app.ts @@ -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 & { password: string }; + +export async function createTestingApp( + moduleDef: TestingAppMeatdata = {} +): Promise { + 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 + ); + + return sessionCookie; +} + +export class TestingApp extends ApplyType() { + private sessionCookie: string | null = null; + private currentUserCookie: string | null = null; + private readonly userCookies: Set = new Set(); + + [Symbol.asyncDispose](): Promise { + 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(query: string, variables?: any): Promise { + 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): Promise { + 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 & { password: string }; + } + + async signup(email: string, override?: Partial) { + 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]; + }, + }); +} diff --git a/packages/backend/server/src/__tests__/utils/testing-module.ts b/packages/backend/server/src/__tests__/utils/testing-module.ts new file mode 100644 index 0000000000..1b730d9922 --- /dev/null +++ b/packages/backend/server/src/__tests__/utils/testing-module.ts @@ -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; + [Symbol.asyncDispose](): Promise; +} + +function dedupeModules(modules: NonNullable) { + 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 { + // 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; +} diff --git a/packages/backend/server/src/__tests__/utils/user.ts b/packages/backend/server/src/__tests__/utils/user.ts index 0925a0658f..bcb922f171 100644 --- a/packages/backend/server/src/__tests__/utils/user.ts +++ b/packages/backend/server/src/__tests__/utils/user.ts @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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', + }); } diff --git a/packages/backend/server/src/__tests__/utils/utils.ts b/packages/backend/server/src/__tests__/utils/utils.ts index 05f2042c47..97a15c915b 100644 --- a/packages/backend/server/src/__tests__/utils/utils.ts +++ b/packages/backend/server/src/__tests__/utils/utils.ts @@ -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; - [Symbol.asyncDispose](): Promise; -}; - -export type TestingApp = INestApplication & { - initTestingDB(): Promise; - [Symbol.asyncDispose](): Promise; - // get the url of the http server, e.g. http://localhost:random-port - getHttpServerUrl(): string; -}; - -function dedupeModules(modules: NonNullable) { - 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 { - // 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) { diff --git a/packages/backend/server/src/__tests__/utils/workspace.ts b/packages/backend/server/src/__tests__/utils/workspace.ts index 9edb5abc3a..9918950812 100644 --- a/packages/backend/server/src/__tests__/utils/workspace.ts +++ b/packages/backend/server/src/__tests__/utils/workspace.ts @@ -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 { - 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 { + 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 { - 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 { - 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; } diff --git a/packages/backend/server/src/__tests__/workspace-invite.e2e.ts b/packages/backend/server/src/__tests__/workspace-invite.e2e.ts index b7765c6f57..824b19e0b1 100644 --- a/packages/backend/server/src/__tests__/workspace-invite.e2e.ts +++ b/packages/backend/server/src/__tests__/workspace-invite.e2e.ts @@ -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'); }); diff --git a/packages/backend/server/src/__tests__/workspace.e2e.ts b/packages/backend/server/src/__tests__/workspace.e2e.ts index 5a7b9f7df9..ce178b2865 100644 --- a/packages/backend/server/src/__tests__/workspace.e2e.ts +++ b/packages/backend/server/src/__tests__/workspace.e2e.ts @@ -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'); diff --git a/packages/backend/server/src/__tests__/workspace/blobs.e2e.ts b/packages/backend/server/src/__tests__/workspace/blobs.e2e.ts index 62beaabca8..963f214cdf 100644 --- a/packages/backend/server/src/__tests__/workspace/blobs.e2e.ts +++ b/packages/backend/server/src/__tests__/workspace/blobs.e2e.ts @@ -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)); }); diff --git a/packages/backend/server/src/__tests__/workspace/controller.spec.ts b/packages/backend/server/src/__tests__/workspace/controller.spec.ts index b37dee6a62..d2f1f190d7 100644 --- a/packages/backend/server/src/__tests__/workspace/controller.spec.ts +++ b/packages/backend/server/src/__tests__/workspace/controller.spec.ts @@ -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; workspace: Sinon.SinonStubbedInstance; }>; 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'); diff --git a/packages/backend/server/src/core/doc-renderer/__tests__/controller.spec.ts b/packages/backend/server/src/core/doc-renderer/__tests__/controller.spec.ts index cb5248d1e8..e05089e9b1 100644 --- a/packages/backend/server/src/core/doc-renderer/__tests__/controller.spec.ts +++ b/packages/backend/server/src/core/doc-renderer/__tests__/controller.spec.ts @@ -2,7 +2,6 @@ import { randomUUID } from 'node:crypto'; import { User, Workspace } from '@prisma/client'; import ava, { TestFn } from 'ava'; -import request from 'supertest'; import { Doc as YDoc } from 'yjs'; import { createTestingApp, type TestingApp } from '../../../__tests__/utils'; @@ -22,7 +21,7 @@ const test = ava as TestFn<{ }>; test.before(async t => { - const { app } = await createTestingApp({ + const app = await createTestingApp({ imports: [ ConfigModule.forRoot({ flavor: { @@ -47,7 +46,7 @@ let user: User; let workspace: Workspace; test.beforeEach(async t => { - t.context.config.docService.endpoint = t.context.app.getHttpServerUrl(); + t.context.config.docService.endpoint = t.context.app.url(); await t.context.app.initTestingDB(); user = await t.context.models.user.create({ email: 'test@affine.pro', @@ -59,7 +58,7 @@ test.after.always(async t => { await t.context.app.close(); }); -test('should render page success', async t => { +test.only('should render page success', async t => { const docId = randomUUID(); const { app, adapter, permission } = t.context; @@ -78,8 +77,6 @@ test('should render page success', async t => { await adapter.pushDocUpdates(workspace.id, docId, updates, user.id); await permission.publishPage(workspace.id, docId); - await request(app.getHttpServer()) - .get(`/workspace/${workspace.id}/${docId}`) - .expect(200); + await app.GET(`/workspace/${workspace.id}/${docId}`).expect(200); t.pass(); }); diff --git a/packages/backend/server/src/core/doc-renderer/__tests__/service.spec.ts b/packages/backend/server/src/core/doc-renderer/__tests__/service.spec.ts index 1fb6d17b75..f7f104773f 100644 --- a/packages/backend/server/src/core/doc-renderer/__tests__/service.spec.ts +++ b/packages/backend/server/src/core/doc-renderer/__tests__/service.spec.ts @@ -21,7 +21,7 @@ const test = ava as TestFn<{ }>; test.before(async t => { - const { app } = await createTestingApp({ + const app = await createTestingApp({ imports: [ ConfigModule.forRoot({ flavor: { @@ -46,7 +46,7 @@ let user: User; let workspace: Workspace; test.beforeEach(async t => { - t.context.config.docService.endpoint = t.context.app.getHttpServerUrl(); + t.context.config.docService.endpoint = t.context.app.url(); await t.context.app.initTestingDB(); user = await t.context.models.user.create({ email: 'test@affine.pro', diff --git a/packages/backend/server/src/core/doc-service/__tests__/controller.spec.ts b/packages/backend/server/src/core/doc-service/__tests__/controller.spec.ts index d6f4e2b01a..1ce67b4fef 100644 --- a/packages/backend/server/src/core/doc-service/__tests__/controller.spec.ts +++ b/packages/backend/server/src/core/doc-service/__tests__/controller.spec.ts @@ -2,7 +2,6 @@ import { randomUUID } from 'node:crypto'; import { User, Workspace } from '@prisma/client'; import ava, { TestFn } from 'ava'; -import request from 'supertest'; import { createTestingApp, type TestingApp } from '../../../__tests__/utils'; import { AppModule } from '../../../app.module'; @@ -17,7 +16,7 @@ const test = ava as TestFn<{ }>; test.before(async t => { - const { app } = await createTestingApp({ + const app = await createTestingApp({ imports: [ConfigModule.forRoot(), AppModule], }); @@ -44,8 +43,8 @@ test.after.always(async t => { test('should forbid access to rpc api without access token', async t => { const { app } = t.context; - await request(app.getHttpServer()) - .get('/rpc/workspaces/123/docs/123') + await app + .GET('/rpc/workspaces/123/docs/123') .expect({ status: 403, code: 'Forbidden', @@ -60,8 +59,8 @@ test('should forbid access to rpc api without access token', async t => { test('should forbid access to rpc api with invalid access token', async t => { const { app } = t.context; - await request(app.getHttpServer()) - .get('/rpc/workspaces/123/docs/123') + await app + .GET('/rpc/workspaces/123/docs/123') .set('x-access-token', 'invalid,wrong-signature') .expect({ status: 403, @@ -79,8 +78,8 @@ test('should 404 when doc not found', async t => { const workspaceId = '123'; const docId = '123'; - await request(app.getHttpServer()) - .get(`/rpc/workspaces/${workspaceId}/docs/${docId}`) + await app + .GET(`/rpc/workspaces/${workspaceId}/docs/${docId}`) .set('x-access-token', t.context.crypto.sign(docId)) .expect({ status: 404, @@ -108,8 +107,8 @@ test('should return doc when found', async t => { }, ]); - const res = await request(app.getHttpServer()) - .get(`/rpc/workspaces/${workspace.id}/docs/${docId}`) + const res = await app + .GET(`/rpc/workspaces/${workspace.id}/docs/${docId}`) .set('x-access-token', t.context.crypto.sign(docId)) .set('x-rpc-trace-id', 'test-trace-id') .expect(200) diff --git a/packages/backend/server/src/core/doc/__tests__/reader-from-database.spec.ts b/packages/backend/server/src/core/doc/__tests__/reader-from-database.spec.ts index 37209c95d2..695954a130 100644 --- a/packages/backend/server/src/core/doc/__tests__/reader-from-database.spec.ts +++ b/packages/backend/server/src/core/doc/__tests__/reader-from-database.spec.ts @@ -17,7 +17,7 @@ const test = ava as TestFn<{ }>; test.before(async t => { - const { app } = await createTestingApp({ + const app = await createTestingApp({ imports: [ConfigModule.forRoot(), AppModule], }); diff --git a/packages/backend/server/src/core/doc/__tests__/reader-from-rpc.spec.ts b/packages/backend/server/src/core/doc/__tests__/reader-from-rpc.spec.ts index a3aa1235a4..60a4f29e37 100644 --- a/packages/backend/server/src/core/doc/__tests__/reader-from-rpc.spec.ts +++ b/packages/backend/server/src/core/doc/__tests__/reader-from-rpc.spec.ts @@ -20,7 +20,7 @@ const test = ava as TestFn<{ }>; test.before(async t => { - const { app } = await createTestingApp({ + const app = await createTestingApp({ imports: [ ConfigModule.forRoot({ flavor: { @@ -44,7 +44,7 @@ let user: User; let workspace: Workspace; test.beforeEach(async t => { - t.context.config.docService.endpoint = t.context.app.getHttpServerUrl(); + t.context.config.docService.endpoint = t.context.app.url(); await t.context.app.initTestingDB(); user = await t.context.models.user.create({ email: 'test@affine.pro', diff --git a/packages/backend/server/src/plugins/copilot/prompt/service.ts b/packages/backend/server/src/plugins/copilot/prompt/service.ts index 8914fe876f..fadf6b220c 100644 --- a/packages/backend/server/src/plugins/copilot/prompt/service.ts +++ b/packages/backend/server/src/plugins/copilot/prompt/service.ts @@ -17,6 +17,7 @@ export class PromptService implements OnModuleInit { constructor(private readonly db: PrismaClient) {} async onModuleInit() { + this.cache.clear(); await refreshPrompts(this.db); }