mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 07:36:42 +08:00
feat!: affine cloud support (#3813)
Co-authored-by: Hongtao Lye <codert.sn@gmail.com> Co-authored-by: liuyi <forehalo@gmail.com> Co-authored-by: LongYinan <lynweklm@gmail.com> Co-authored-by: X1a0t <405028157@qq.com> Co-authored-by: JimmFly <yangjinfei001@gmail.com> Co-authored-by: Peng Xiao <pengxiao@outlook.com> Co-authored-by: xiaodong zuo <53252747+zuoxiaodong0815@users.noreply.github.com> Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com> Co-authored-by: danielchim <kahungchim@gmail.com>
This commit is contained in:
@@ -30,6 +30,7 @@ describe('AppModule', () => {
|
||||
password: await hash('123456'),
|
||||
},
|
||||
});
|
||||
await client.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
/// <reference types="../global.d.ts" />
|
||||
import { ok } from 'node:assert';
|
||||
import { beforeEach, test } from 'node:test';
|
||||
import { equal } from 'node:assert';
|
||||
import { afterEach, beforeEach, test } from 'node:test';
|
||||
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { ConfigModule } from '../config';
|
||||
import { GqlModule } from '../graphql.module';
|
||||
import { MetricsModule } from '../metrics';
|
||||
import { AuthModule } from '../modules/auth';
|
||||
import { AuthService } from '../modules/auth/service';
|
||||
import { PrismaModule } from '../prisma';
|
||||
|
||||
let auth: AuthService;
|
||||
let module: TestingModule;
|
||||
|
||||
// cleanup database before each test
|
||||
beforeEach(async () => {
|
||||
@@ -21,7 +23,7 @@ beforeEach(async () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
module = await Test.createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
auth: {
|
||||
@@ -33,37 +35,50 @@ beforeEach(async () => {
|
||||
PrismaModule,
|
||||
GqlModule,
|
||||
AuthModule,
|
||||
MetricsModule,
|
||||
],
|
||||
}).compile();
|
||||
auth = module.get(AuthService);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('should be able to register and signIn', async () => {
|
||||
await auth.register('Alex Yang', 'alexyang@example.org', '123456');
|
||||
await auth.signUp('Alex Yang', 'alexyang@example.org', '123456');
|
||||
await auth.signIn('alexyang@example.org', '123456');
|
||||
});
|
||||
|
||||
test('should be able to verify', async () => {
|
||||
await auth.register('Alex Yang', 'alexyang@example.org', '123456');
|
||||
await auth.signUp('Alex Yang', 'alexyang@example.org', '123456');
|
||||
await auth.signIn('alexyang@example.org', '123456');
|
||||
const date = new Date();
|
||||
|
||||
const user = {
|
||||
id: '1',
|
||||
name: 'Alex Yang',
|
||||
email: 'alexyang@example.org',
|
||||
createdAt: new Date(),
|
||||
emailVerified: date,
|
||||
createdAt: date,
|
||||
avatarUrl: '',
|
||||
};
|
||||
{
|
||||
const token = await auth.sign(user);
|
||||
const claim = await auth.verify(token);
|
||||
ok(claim.id === '1');
|
||||
ok(claim.name === 'Alex Yang');
|
||||
ok(claim.email === 'alexyang@example.org');
|
||||
equal(claim.id, '1');
|
||||
equal(claim.name, 'Alex Yang');
|
||||
equal(claim.email, 'alexyang@example.org');
|
||||
equal(claim.emailVerified?.toISOString(), date.toISOString());
|
||||
equal(claim.createdAt.toISOString(), date.toISOString());
|
||||
}
|
||||
{
|
||||
const token = await auth.refresh(user);
|
||||
const claim = await auth.verify(token);
|
||||
ok(claim.id === '1');
|
||||
ok(claim.name === 'Alex Yang');
|
||||
ok(claim.email === 'alexyang@example.org');
|
||||
equal(claim.id, '1');
|
||||
equal(claim.name, 'Alex Yang');
|
||||
equal(claim.email, 'alexyang@example.org');
|
||||
equal(claim.emailVerified?.toISOString(), date.toISOString());
|
||||
equal(claim.createdAt.toISOString(), date.toISOString());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { deepEqual, equal, ok } from 'node:assert';
|
||||
import { afterEach, beforeEach, mock, test } from 'node:test';
|
||||
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { register } from 'prom-client';
|
||||
import * as Sinon from 'sinon';
|
||||
import { Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { Config, ConfigModule } from '../config';
|
||||
import { MetricsModule } from '../metrics';
|
||||
import { DocManager, DocModule } from '../modules/doc';
|
||||
import { PrismaModule, PrismaService } from '../prisma';
|
||||
import { flushDB } from './utils';
|
||||
|
||||
const createModule = () => {
|
||||
return Test.createTestingModule({
|
||||
imports: [
|
||||
PrismaModule,
|
||||
MetricsModule,
|
||||
ConfigModule.forRoot(),
|
||||
DocModule.forRoot(),
|
||||
],
|
||||
}).compile();
|
||||
};
|
||||
|
||||
test('Doc Module', async t => {
|
||||
let app: INestApplication;
|
||||
let m: TestingModule;
|
||||
let timer: Sinon.SinonFakeTimers;
|
||||
|
||||
// cleanup database before each test
|
||||
beforeEach(async () => {
|
||||
timer = Sinon.useFakeTimers({
|
||||
toFake: ['setInterval'],
|
||||
});
|
||||
await flushDB();
|
||||
m = await createModule();
|
||||
app = m.createNestApplication();
|
||||
app.enableShutdownHooks();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
timer.restore();
|
||||
});
|
||||
|
||||
await t.test('should setup update poll interval', async () => {
|
||||
register.clear();
|
||||
const m = await createModule();
|
||||
const manager = m.get(DocManager);
|
||||
const fake = mock.method(manager, 'setup');
|
||||
|
||||
await m.createNestApplication().init();
|
||||
|
||||
equal(fake.mock.callCount(), 1);
|
||||
// @ts-expect-error private member
|
||||
ok(manager.job);
|
||||
});
|
||||
|
||||
await t.test('should be able to stop poll', async () => {
|
||||
const manager = m.get(DocManager);
|
||||
const fake = mock.method(manager, 'destroy');
|
||||
|
||||
await app.close();
|
||||
|
||||
equal(fake.mock.callCount(), 1);
|
||||
// @ts-expect-error private member
|
||||
equal(manager.job, null);
|
||||
});
|
||||
|
||||
await t.test('should poll when intervel due', async () => {
|
||||
const manager = m.get(DocManager);
|
||||
const interval = m.get(Config).doc.manager.updatePollInterval;
|
||||
|
||||
let resolve: any;
|
||||
const fake = mock.method(manager, 'apply', () => {
|
||||
return new Promise(_resolve => {
|
||||
resolve = _resolve;
|
||||
});
|
||||
});
|
||||
|
||||
timer.tick(interval);
|
||||
equal(fake.mock.callCount(), 1);
|
||||
|
||||
// busy
|
||||
timer.tick(interval);
|
||||
// @ts-expect-error private member
|
||||
equal(manager.busy, true);
|
||||
equal(fake.mock.callCount(), 1);
|
||||
|
||||
resolve();
|
||||
await timer.tickAsync(1);
|
||||
|
||||
// @ts-expect-error private member
|
||||
equal(manager.busy, false);
|
||||
timer.tick(interval);
|
||||
equal(fake.mock.callCount(), 2);
|
||||
});
|
||||
|
||||
await t.test('should merge update when intervel due', async () => {
|
||||
const db = m.get(PrismaService);
|
||||
const manager = m.get(DocManager);
|
||||
|
||||
const doc = new YDoc();
|
||||
const text = doc.getText('content');
|
||||
text.insert(0, 'hello');
|
||||
const update = encodeStateAsUpdate(doc);
|
||||
|
||||
const ws = await db.workspace.create({
|
||||
data: {
|
||||
id: '1',
|
||||
public: false,
|
||||
},
|
||||
});
|
||||
|
||||
await db.update.createMany({
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
workspaceId: '1',
|
||||
blob: Buffer.from([0, 0]),
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
workspaceId: '1',
|
||||
blob: Buffer.from(update),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await manager.apply();
|
||||
|
||||
deepEqual(await manager.getLatestUpdate(ws.id, '1'), update);
|
||||
|
||||
let appendUpdate = Buffer.from([]);
|
||||
doc.on('update', update => {
|
||||
appendUpdate = Buffer.from(update);
|
||||
});
|
||||
text.insert(5, 'world');
|
||||
|
||||
await db.update.create({
|
||||
data: {
|
||||
workspaceId: ws.id,
|
||||
id: '1',
|
||||
blob: appendUpdate,
|
||||
},
|
||||
});
|
||||
|
||||
await manager.apply();
|
||||
|
||||
deepEqual(
|
||||
await manager.getLatestUpdate(ws.id, '1'),
|
||||
encodeStateAsUpdate(doc)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { ok } from 'node:assert';
|
||||
import { afterEach, beforeEach, describe, it } from 'node:test';
|
||||
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import { MailService } from '../modules/auth/mailer';
|
||||
import { createWorkspace, getInviteInfo, inviteUser, signUp } from './utils';
|
||||
|
||||
describe('Mail Module', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
const client = new PrismaClient();
|
||||
|
||||
let mail: MailService;
|
||||
|
||||
// cleanup database before each test
|
||||
beforeEach(async () => {
|
||||
await client.$connect();
|
||||
await client.user.deleteMany({});
|
||||
await client.snapshot.deleteMany({});
|
||||
await client.update.deleteMany({});
|
||||
await client.workspace.deleteMany({});
|
||||
await client.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
app = module.createNestApplication();
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
|
||||
mail = module.get(MailService);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('should send invite email', async () => {
|
||||
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 inviteId = await inviteUser(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id,
|
||||
u2.email,
|
||||
'Admin'
|
||||
);
|
||||
|
||||
const inviteInfo = await getInviteInfo(app, u1.token.token, inviteId);
|
||||
|
||||
const resp = await mail.sendInviteEmail(
|
||||
'production@toeverything.info',
|
||||
inviteId,
|
||||
{
|
||||
workspace: {
|
||||
id: inviteInfo.workspace.id,
|
||||
name: inviteInfo.workspace.name,
|
||||
avatar: '',
|
||||
},
|
||||
user: {
|
||||
avatar: inviteInfo.user?.avatarUrl || '',
|
||||
name: inviteInfo.user?.name || '',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
ok(resp.accepted.length === 1, 'failed to send invite email');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ok } from 'node:assert';
|
||||
import { afterEach, beforeEach, test } from 'node:test';
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { register } from 'prom-client';
|
||||
|
||||
import { MetricsModule } from '../metrics';
|
||||
import { Metrics } from '../metrics/metrics';
|
||||
import { PrismaModule } from '../prisma';
|
||||
|
||||
let metrics: Metrics;
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [MetricsModule, PrismaModule],
|
||||
}).compile();
|
||||
|
||||
metrics = module.get(Metrics);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('should be able to increment counter', async () => {
|
||||
metrics.socketIOEventCounter(1, { event: 'client-handshake' });
|
||||
const socketIOCounterMetric =
|
||||
await register.getSingleMetric('socket_io_counter');
|
||||
ok(socketIOCounterMetric);
|
||||
|
||||
ok(
|
||||
JSON.stringify((await socketIOCounterMetric.get()).values) ===
|
||||
'[{"value":1,"labels":{"event":"client-handshake"}}]'
|
||||
);
|
||||
});
|
||||
|
||||
test('should be able to timer', async () => {
|
||||
const endTimer = metrics.socketIOEventTimer({ event: 'client-handshake' });
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
endTimer();
|
||||
|
||||
const endTimer2 = metrics.socketIOEventTimer({ event: 'client-handshake' });
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
endTimer2();
|
||||
|
||||
const socketIOTimerMetric = await register.getSingleMetric('socket_io_timer');
|
||||
ok(socketIOTimerMetric);
|
||||
|
||||
const observations = (await socketIOTimerMetric.get()).values;
|
||||
|
||||
for (const observation of observations) {
|
||||
if (
|
||||
observation.labels.event === 'client-handshake' &&
|
||||
'quantile' in observation.labels
|
||||
) {
|
||||
ok(observation.value >= 0.05);
|
||||
ok(observation.value <= 0.15);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { ok } from 'node:assert';
|
||||
import { afterEach, beforeEach, describe, it } from 'node:test';
|
||||
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import { currentUser, signUp } from './utils';
|
||||
|
||||
describe('User Module', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
// cleanup database before each test
|
||||
beforeEach(async () => {
|
||||
const client = new PrismaClient();
|
||||
await client.$connect();
|
||||
await client.user.deleteMany({});
|
||||
await client.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
app = module.createNestApplication();
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('should register a user', async () => {
|
||||
const user = await signUp(app, 'u1', 'u1@affine.pro', '123456');
|
||||
ok(typeof user.id === 'string', 'user.id is not a string');
|
||||
ok(user.name === 'u1', 'user.name is not valid');
|
||||
ok(user.email === 'u1@affine.pro', 'user.email is not valid');
|
||||
});
|
||||
|
||||
it('should get current user', async () => {
|
||||
const user = await signUp(app, 'u1', 'u1@affine.pro', '123456');
|
||||
const currUser = await currentUser(app, user.token.token);
|
||||
ok(currUser.id === user.id, 'user.id is not valid');
|
||||
ok(currUser.name === user.name, 'user.name is not valid');
|
||||
ok(currUser.email === user.email, 'user.email is not valid');
|
||||
ok(currUser.hasPassword, 'currUser.hasPassword is not valid');
|
||||
});
|
||||
|
||||
it('should be able to delete user', async () => {
|
||||
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);
|
||||
const current = await currentUser(app, user.token.token);
|
||||
ok(current == null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,465 @@
|
||||
import type { INestApplication, LoggerService } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import type { TokenType } from '../modules/auth';
|
||||
import type { UserType } from '../modules/users';
|
||||
import type { InvitationType, WorkspaceType } from '../modules/workspaces';
|
||||
|
||||
export class NestDebugLogger implements LoggerService {
|
||||
log(message: string): any {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
error(message: string, trace: string): any {
|
||||
console.error(message, trace);
|
||||
}
|
||||
|
||||
warn(message: string): any {
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
debug(message: string): any {
|
||||
console.debug(message);
|
||||
}
|
||||
|
||||
verbose(message: string): any {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
const gql = '/graphql';
|
||||
|
||||
async function signUp(
|
||||
app: INestApplication,
|
||||
name: string,
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<UserType & { token: TokenType }> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
signUp(name: "${name}", email: "${email}", password: "${password}") {
|
||||
id, name, email, token { token }
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.signUp;
|
||||
}
|
||||
|
||||
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, createdAt, hasPassword
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body?.data?.currentUser;
|
||||
}
|
||||
|
||||
async function createWorkspace(
|
||||
app: INestApplication,
|
||||
token: string
|
||||
): Promise<WorkspaceType> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.field(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
name: 'createWorkspace',
|
||||
query: `mutation createWorkspace($init: Upload!) {
|
||||
createWorkspace(init: $init) {
|
||||
id
|
||||
}
|
||||
}`,
|
||||
variables: { init: null },
|
||||
})
|
||||
)
|
||||
.field('map', JSON.stringify({ '0': ['variables.init'] }))
|
||||
.attach('0', Buffer.from([0, 0]), 'init.data')
|
||||
.expect(200);
|
||||
return res.body.data.createWorkspace;
|
||||
}
|
||||
|
||||
export async function getWorkspaceSharedPages(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string
|
||||
): Promise<string[]> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
workspace(id: "${workspaceId}") {
|
||||
sharedPages
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.workspace.sharedPages;
|
||||
}
|
||||
|
||||
async function getWorkspace(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string
|
||||
): Promise<WorkspaceType> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
workspace(id: "${workspaceId}") {
|
||||
id, members { id, name, email, permission, inviteId }
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.workspace;
|
||||
}
|
||||
|
||||
async function getPublicWorkspace(
|
||||
app: INestApplication,
|
||||
workspaceId: string
|
||||
): Promise<WorkspaceType> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
publicWorkspace(id: "${workspaceId}") {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.publicWorkspace;
|
||||
}
|
||||
|
||||
async function updateWorkspace(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string,
|
||||
isPublic: boolean
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
updateWorkspace(input: { id: "${workspaceId}", public: ${isPublic} }) {
|
||||
public
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.updateWorkspace.public;
|
||||
}
|
||||
|
||||
async function inviteUser(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string,
|
||||
email: string,
|
||||
permission: string,
|
||||
sendInviteMail = false
|
||||
): Promise<string> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
invite(workspaceId: "${workspaceId}", email: "${email}", permission: ${permission}, sendInviteMail: ${sendInviteMail})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.invite;
|
||||
}
|
||||
|
||||
async function acceptInviteById(
|
||||
app: INestApplication,
|
||||
workspaceId: string,
|
||||
inviteId: string
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
acceptInviteById(workspaceId: "${workspaceId}", inviteId: "${inviteId}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.acceptInviteById;
|
||||
}
|
||||
|
||||
async function acceptInvite(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
acceptInvite(workspaceId: "${workspaceId}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.acceptInvite;
|
||||
}
|
||||
|
||||
async function leaveWorkspace(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
leaveWorkspace(workspaceId: "${workspaceId}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.leaveWorkspace;
|
||||
}
|
||||
|
||||
async function revokeUser(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string,
|
||||
userId: string
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
revoke(workspaceId: "${workspaceId}", userId: "${userId}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.revoke;
|
||||
}
|
||||
|
||||
async function sharePage(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string,
|
||||
pageId: string
|
||||
): Promise<boolean | 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 {
|
||||
sharePage(workspaceId: "${workspaceId}", pageId: "${pageId}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.errors?.[0]?.message || res.body.data?.sharePage;
|
||||
}
|
||||
|
||||
async function revokePage(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string,
|
||||
pageId: string
|
||||
): Promise<boolean | 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 {
|
||||
revokePage(workspaceId: "${workspaceId}", pageId: "${pageId}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.errors?.[0]?.message || res.body.data?.revokePage;
|
||||
}
|
||||
|
||||
async function listBlobs(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string
|
||||
): Promise<string[]> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
listBlobs(workspaceId: "${workspaceId}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.listBlobs;
|
||||
}
|
||||
|
||||
async function setBlob(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string,
|
||||
buffer: Buffer
|
||||
): Promise<string> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.field(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
name: 'setBlob',
|
||||
query: `mutation setBlob($blob: Upload!) {
|
||||
setBlob(workspaceId: "${workspaceId}", blob: $blob)
|
||||
}`,
|
||||
variables: { blob: null },
|
||||
})
|
||||
)
|
||||
.field('map', JSON.stringify({ '0': ['variables.blob'] }))
|
||||
.attach('0', buffer, 'blob.data')
|
||||
.expect(200);
|
||||
return res.body.data.setBlob;
|
||||
}
|
||||
|
||||
async function flushDB() {
|
||||
const client = new PrismaClient();
|
||||
await client.$connect();
|
||||
const result: { tablename: string }[] =
|
||||
await client.$queryRaw`SELECT tablename
|
||||
FROM pg_catalog.pg_tables
|
||||
WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'`;
|
||||
|
||||
// remove all table data
|
||||
await client.$executeRawUnsafe(
|
||||
`TRUNCATE TABLE ${result
|
||||
.map(({ tablename }) => tablename)
|
||||
.filter(name => !name.includes('migrations'))
|
||||
.join(', ')}`
|
||||
);
|
||||
|
||||
await client.$disconnect();
|
||||
}
|
||||
|
||||
async function createTestApp() {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
const app = module.createNestApplication();
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
return app;
|
||||
}
|
||||
|
||||
async function getInviteInfo(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
inviteId: string
|
||||
): Promise<InvitationType> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
getInviteInfo(inviteId: "${inviteId}") {
|
||||
workspace {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
}
|
||||
user {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.workspace;
|
||||
}
|
||||
|
||||
export {
|
||||
acceptInvite,
|
||||
acceptInviteById,
|
||||
createTestApp,
|
||||
createWorkspace,
|
||||
currentUser,
|
||||
flushDB,
|
||||
getInviteInfo,
|
||||
getPublicWorkspace,
|
||||
getWorkspace,
|
||||
inviteUser,
|
||||
leaveWorkspace,
|
||||
listBlobs,
|
||||
revokePage,
|
||||
revokeUser,
|
||||
setBlob,
|
||||
sharePage,
|
||||
signUp,
|
||||
updateWorkspace,
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { deepEqual, ok } from 'node:assert';
|
||||
import { afterEach, beforeEach, describe, it } from 'node:test';
|
||||
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import { createWorkspace, listBlobs, setBlob, signUp } from './utils';
|
||||
|
||||
describe('Workspace Module - Blobs', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
const client = new PrismaClient();
|
||||
|
||||
// cleanup database before each test
|
||||
beforeEach(async () => {
|
||||
await client.$connect();
|
||||
await client.user.deleteMany({});
|
||||
await client.snapshot.deleteMany({});
|
||||
await client.update.deleteMany({});
|
||||
await client.workspace.deleteMany({});
|
||||
await client.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
app = module.createNestApplication();
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('should list blobs', async () => {
|
||||
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||
|
||||
const workspace = await createWorkspace(app, u1.token.token);
|
||||
const blobs = await listBlobs(app, u1.token.token, workspace.id);
|
||||
ok(blobs.length === 0, 'failed to list blobs');
|
||||
|
||||
const buffer = Buffer.from([0, 0]);
|
||||
const hash = await setBlob(app, u1.token.token, workspace.id, buffer);
|
||||
|
||||
const ret = await listBlobs(app, u1.token.token, workspace.id);
|
||||
ok(ret.length === 1, 'failed to list blobs');
|
||||
ok(ret[0] === hash, 'failed to list blobs');
|
||||
const server = app.getHttpServer();
|
||||
|
||||
const token = u1.token.token;
|
||||
const response = await request(server)
|
||||
.get(`/api/workspaces/${workspace.id}/blobs/${hash}`)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.buffer();
|
||||
|
||||
deepEqual(response.body, buffer, 'failed to get blob');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
import { ok } from 'node:assert';
|
||||
import { afterEach, beforeEach, describe, it } from 'node:test';
|
||||
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import { MailService } from '../modules/auth/mailer';
|
||||
import { AuthService } from '../modules/auth/service';
|
||||
import {
|
||||
acceptInvite,
|
||||
acceptInviteById,
|
||||
createWorkspace,
|
||||
getWorkspace,
|
||||
inviteUser,
|
||||
leaveWorkspace,
|
||||
revokeUser,
|
||||
signUp,
|
||||
} from './utils';
|
||||
|
||||
describe('Workspace Module - invite', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
const client = new PrismaClient();
|
||||
|
||||
let auth: AuthService;
|
||||
let mail: MailService;
|
||||
|
||||
// cleanup database before each test
|
||||
beforeEach(async () => {
|
||||
await client.$connect();
|
||||
await client.user.deleteMany({});
|
||||
await client.snapshot.deleteMany({});
|
||||
await client.update.deleteMany({});
|
||||
await client.workspace.deleteMany({});
|
||||
await client.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
app = module.createNestApplication();
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
|
||||
auth = module.get(AuthService);
|
||||
mail = module.get(MailService);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('should invite a user', async () => {
|
||||
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 invite = await inviteUser(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id,
|
||||
u2.email,
|
||||
'Admin'
|
||||
);
|
||||
ok(!!invite, 'failed to invite user');
|
||||
});
|
||||
|
||||
it('should accept an invite', async () => {
|
||||
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);
|
||||
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');
|
||||
|
||||
const accept = await acceptInvite(app, u2.token.token, workspace.id);
|
||||
ok(accept === true, 'failed to accept invite');
|
||||
|
||||
const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id);
|
||||
const currMember = currWorkspace.members.find(u => u.email === u2.email);
|
||||
ok(currMember !== undefined, 'failed to invite user');
|
||||
ok(currMember.id === u2.id, 'failed to invite user');
|
||||
ok(!currMember.accepted, 'failed to invite user');
|
||||
});
|
||||
|
||||
it('should leave a workspace', async () => {
|
||||
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);
|
||||
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');
|
||||
await acceptInvite(app, u2.token.token, workspace.id);
|
||||
|
||||
const leave = await leaveWorkspace(app, u2.token.token, workspace.id);
|
||||
ok(leave === true, 'failed to leave workspace');
|
||||
});
|
||||
|
||||
it('should revoke a user', async () => {
|
||||
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);
|
||||
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');
|
||||
|
||||
const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id);
|
||||
ok(currWorkspace.members.length === 2, 'failed to invite user');
|
||||
|
||||
const revoke = await revokeUser(app, u1.token.token, workspace.id, u2.id);
|
||||
ok(revoke === true, 'failed to revoke user');
|
||||
});
|
||||
|
||||
it('should create user if not exist', async () => {
|
||||
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||
|
||||
const workspace = await createWorkspace(app, u1.token.token);
|
||||
|
||||
await inviteUser(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id,
|
||||
'u2@affine.pro',
|
||||
'Admin'
|
||||
);
|
||||
|
||||
const user = await auth.getUserByEmail('u2@affine.pro');
|
||||
ok(user !== undefined, 'failed to create user');
|
||||
ok(user?.name === 'Unnamed', 'failed to create user');
|
||||
});
|
||||
|
||||
it('should invite a user by link', async () => {
|
||||
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 invite = await inviteUser(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id,
|
||||
u2.email,
|
||||
'Admin'
|
||||
);
|
||||
|
||||
const accept = await acceptInviteById(app, workspace.id, invite);
|
||||
ok(accept === true, 'failed to accept invite');
|
||||
|
||||
const invite1 = await inviteUser(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id,
|
||||
u2.email,
|
||||
'Admin'
|
||||
);
|
||||
|
||||
ok(invite === invite1, 'repeat the invitation must return same id');
|
||||
|
||||
const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id);
|
||||
const currMember = currWorkspace.members.find(u => u.email === u2.email);
|
||||
ok(currMember !== undefined, 'failed to invite user');
|
||||
ok(currMember.inviteId === invite, 'failed to check invite id');
|
||||
});
|
||||
|
||||
it('should send invite email', async () => {
|
||||
if (mail.hasConfigured()) {
|
||||
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||
const u2 = await signUp(app, 'test', 'production@toeverything.info', '1');
|
||||
|
||||
const workspace = await createWorkspace(app, u1.token.token);
|
||||
await inviteUser(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id,
|
||||
u2.email,
|
||||
'Admin',
|
||||
true
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ok } from 'node:assert';
|
||||
import { deepEqual, ok, rejects } from 'node:assert';
|
||||
import { afterEach, beforeEach, describe, it } from 'node:test';
|
||||
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
@@ -9,20 +9,30 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import type { TokenType } from '../modules/auth';
|
||||
import type { UserType } from '../modules/users';
|
||||
import type { WorkspaceType } from '../modules/workspaces';
|
||||
|
||||
const gql = '/graphql';
|
||||
import {
|
||||
acceptInvite,
|
||||
createWorkspace,
|
||||
getPublicWorkspace,
|
||||
getWorkspaceSharedPages,
|
||||
inviteUser,
|
||||
revokePage,
|
||||
sharePage,
|
||||
signUp,
|
||||
updateWorkspace,
|
||||
} from './utils';
|
||||
|
||||
describe('Workspace Module', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
const client = new PrismaClient();
|
||||
|
||||
// cleanup database before each test
|
||||
beforeEach(async () => {
|
||||
const client = new PrismaClient();
|
||||
await client.$connect();
|
||||
await client.user.deleteMany({});
|
||||
await client.update.deleteMany({});
|
||||
await client.snapshot.deleteMany({});
|
||||
await client.workspace.deleteMany({});
|
||||
await client.$disconnect();
|
||||
});
|
||||
|
||||
@@ -44,183 +54,177 @@ describe('Workspace Module', () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function registerUser(
|
||||
name: string,
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<UserType & { token: TokenType }> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
register(name: "${name}", email: "${email}", password: "${password}") {
|
||||
id, name, email, token { token }
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.register;
|
||||
}
|
||||
|
||||
async function createWorkspace(token: string): Promise<WorkspaceType> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.field(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
name: 'createWorkspace',
|
||||
query: `mutation createWorkspace($init: Upload!) {
|
||||
createWorkspace(init: $init) {
|
||||
id
|
||||
}
|
||||
}`,
|
||||
variables: { init: null },
|
||||
})
|
||||
)
|
||||
.field('map', JSON.stringify({ '0': ['variables.init'] }))
|
||||
.attach('0', Buffer.from([0, 0]), 'init.data')
|
||||
.expect(200);
|
||||
return res.body.data.createWorkspace;
|
||||
}
|
||||
|
||||
async function inviteUser(
|
||||
token: string,
|
||||
workspaceId: string,
|
||||
email: string,
|
||||
permission: string
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
invite(workspaceId: "${workspaceId}", email: "${email}", permission: ${permission})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.invite;
|
||||
}
|
||||
|
||||
async function acceptInvite(
|
||||
token: string,
|
||||
workspaceId: string
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
acceptInvite(workspaceId: "${workspaceId}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.acceptInvite;
|
||||
}
|
||||
|
||||
async function leaveWorkspace(
|
||||
token: string,
|
||||
workspaceId: string
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
leaveWorkspace(workspaceId: "${workspaceId}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.leaveWorkspace;
|
||||
}
|
||||
|
||||
async function revokeUser(
|
||||
token: string,
|
||||
workspaceId: string,
|
||||
userId: string
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
revoke(workspaceId: "${workspaceId}", userId: "${userId}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.revoke;
|
||||
}
|
||||
|
||||
it('should register a user', async () => {
|
||||
const user = await registerUser('u1', 'u1@affine.pro', '123456');
|
||||
const user = await signUp(app, 'u1', 'u1@affine.pro', '123456');
|
||||
ok(typeof user.id === 'string', 'user.id is not a string');
|
||||
ok(user.name === 'u1', 'user.name is not valid');
|
||||
ok(user.email === 'u1@affine.pro', 'user.email is not valid');
|
||||
});
|
||||
|
||||
it('should create a workspace', async () => {
|
||||
const user = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const user = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||
|
||||
const workspace = await createWorkspace(user.token.token);
|
||||
const workspace = await createWorkspace(app, user.token.token);
|
||||
ok(typeof workspace.id === 'string', 'workspace.id is not a string');
|
||||
});
|
||||
|
||||
it('should invite a user', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
it('should can publish workspace', async () => {
|
||||
const user = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||
const workspace = await createWorkspace(app, user.token.token);
|
||||
|
||||
const workspace = await createWorkspace(u1.token.token);
|
||||
|
||||
const invite = await inviteUser(
|
||||
u1.token.token,
|
||||
const isPublic = await updateWorkspace(
|
||||
app,
|
||||
user.token.token,
|
||||
workspace.id,
|
||||
u2.email,
|
||||
'Admin'
|
||||
true
|
||||
);
|
||||
ok(invite === true, 'failed to invite user');
|
||||
ok(isPublic === true, 'failed to publish workspace');
|
||||
|
||||
const isPrivate = await updateWorkspace(
|
||||
app,
|
||||
user.token.token,
|
||||
workspace.id,
|
||||
false
|
||||
);
|
||||
ok(isPrivate === false, 'failed to unpublish workspace');
|
||||
});
|
||||
|
||||
it('should accept an invite', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
it('should can read published workspace', async () => {
|
||||
const user = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||
const workspace = await createWorkspace(app, user.token.token);
|
||||
|
||||
const workspace = await createWorkspace(u1.token.token);
|
||||
await inviteUser(u1.token.token, workspace.id, u2.email, 'Admin');
|
||||
rejects(
|
||||
getPublicWorkspace(app, 'not_exists_ws'),
|
||||
'must not get not exists workspace'
|
||||
);
|
||||
rejects(
|
||||
getPublicWorkspace(app, workspace.id),
|
||||
'must not get private workspace'
|
||||
);
|
||||
|
||||
const accept = await acceptInvite(u2.token.token, workspace.id);
|
||||
ok(accept === true, 'failed to accept invite');
|
||||
await updateWorkspace(app, user.token.token, workspace.id, true);
|
||||
|
||||
const publicWorkspace = await getPublicWorkspace(app, workspace.id);
|
||||
ok(publicWorkspace.id === workspace.id, 'failed to get public workspace');
|
||||
});
|
||||
|
||||
it('should leave a workspace', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
it('should share a page', async () => {
|
||||
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
|
||||
|
||||
const workspace = await createWorkspace(u1.token.token);
|
||||
await inviteUser(u1.token.token, workspace.id, u2.email, 'Admin');
|
||||
await acceptInvite(u2.token.token, workspace.id);
|
||||
const workspace = await createWorkspace(app, u1.token.token);
|
||||
|
||||
const leave = await leaveWorkspace(u2.token.token, workspace.id);
|
||||
ok(leave === true, 'failed to leave workspace');
|
||||
const share = await sharePage(app, u1.token.token, workspace.id, 'page1');
|
||||
ok(share === true, 'failed to share page');
|
||||
const pages = await getWorkspaceSharedPages(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id
|
||||
);
|
||||
ok(pages.length === 1, 'failed to get shared pages');
|
||||
ok(pages[0] === 'page1', 'failed to get shared page: page1');
|
||||
|
||||
const msg1 = await sharePage(app, u2.token.token, workspace.id, 'page2');
|
||||
ok(msg1 === 'Permission denied', 'unauthorized user can share page');
|
||||
const msg2 = await revokePage(
|
||||
app,
|
||||
u2.token.token,
|
||||
'not_exists_ws',
|
||||
'page2'
|
||||
);
|
||||
ok(msg2 === 'Permission denied', 'unauthorized user can share page');
|
||||
|
||||
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');
|
||||
await acceptInvite(app, u2.token.token, workspace.id);
|
||||
const invited = await sharePage(app, u2.token.token, workspace.id, 'page2');
|
||||
ok(invited === true, 'failed to share page');
|
||||
|
||||
const revoke = await revokePage(app, u1.token.token, workspace.id, 'page1');
|
||||
ok(revoke === true, 'failed to revoke page');
|
||||
const pages2 = await getWorkspaceSharedPages(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id
|
||||
);
|
||||
ok(pages2.length === 1, 'failed to get shared pages');
|
||||
ok(pages2[0] === 'page2', 'failed to get shared page: page2');
|
||||
|
||||
const msg3 = await revokePage(app, u1.token.token, workspace.id, 'page3');
|
||||
ok(msg3 === false, 'can revoke non-exists page');
|
||||
|
||||
const msg4 = await revokePage(app, u1.token.token, workspace.id, 'page2');
|
||||
ok(msg4 === true, 'failed to revoke page');
|
||||
const page3 = await getWorkspaceSharedPages(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id
|
||||
);
|
||||
ok(page3.length === 0, 'failed to get shared pages');
|
||||
});
|
||||
|
||||
it('should revoke a user', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
it('should can get workspace doc', async () => {
|
||||
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 workspace = await createWorkspace(u1.token.token);
|
||||
await inviteUser(u1.token.token, workspace.id, u2.email, 'Admin');
|
||||
const res1 = await request(app.getHttpServer())
|
||||
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
||||
.auth(u1.token.token, { type: 'bearer' })
|
||||
.expect(200)
|
||||
.type('application/octet-stream');
|
||||
|
||||
const revoke = await revokeUser(u1.token.token, workspace.id, u2.id);
|
||||
ok(revoke === true, 'failed to revoke user');
|
||||
deepEqual(
|
||||
res1.body,
|
||||
Buffer.from([0, 0]),
|
||||
'failed to get doc with u1 token'
|
||||
);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.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' })
|
||||
.expect(403);
|
||||
|
||||
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
||||
.auth(u2.token.token, { type: 'bearer' })
|
||||
.expect(403);
|
||||
|
||||
await acceptInvite(app, u2.token.token, workspace.id);
|
||||
const res2 = await request(app.getHttpServer())
|
||||
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
||||
.auth(u2.token.token, { type: 'bearer' })
|
||||
.expect(200)
|
||||
.type('application/octet-stream');
|
||||
|
||||
deepEqual(
|
||||
res2.body,
|
||||
Buffer.from([0, 0]),
|
||||
'failed to get doc with u2 token'
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to get public workspace doc', async () => {
|
||||
const user = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||
const workspace = await createWorkspace(app, user.token.token);
|
||||
|
||||
const isPublic = await updateWorkspace(
|
||||
app,
|
||||
user.token.token,
|
||||
workspace.id,
|
||||
true
|
||||
);
|
||||
|
||||
ok(isPublic === true, 'failed to publish workspace');
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
||||
.expect(200)
|
||||
.type('application/octet-stream');
|
||||
|
||||
deepEqual(res.body, Buffer.from([0, 0]), 'failed to get public doc');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user