mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 20:38:52 +00:00
test(server): move tests out of src folder (#4366)
This commit is contained in:
@@ -19,7 +19,6 @@ import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
|
||||
import { PrismaInstrumentation } from '@prisma/instrumentation';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { static as staticMiddleware } from 'express';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
|
||||
import { AppModule } from './app';
|
||||
|
||||
@@ -16,8 +16,7 @@ import { NextAuthOptionsProvide } from './next-auth-options';
|
||||
import { AuthService } from './service';
|
||||
|
||||
export function getUserFromContext(context: ExecutionContext) {
|
||||
const req = getRequestResponseFromContext(context).req;
|
||||
return req.user;
|
||||
return getRequestResponseFromContext(context).req.user;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import type { User } from '@prisma/client';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
|
||||
import { PrismaService } from '../../prisma/service';
|
||||
@@ -141,17 +140,16 @@ export class UserResolver {
|
||||
description: 'Upload user avatar',
|
||||
})
|
||||
async uploadAvatar(
|
||||
@Args('id') id: string,
|
||||
@CurrentUser() user: UserType,
|
||||
@Args({ name: 'avatar', type: () => GraphQLUpload })
|
||||
avatar: FileUpload
|
||||
) {
|
||||
const user = await this.users.findUserById(id);
|
||||
if (!user) {
|
||||
throw new BadRequestException(`User ${id} not found`);
|
||||
throw new BadRequestException(`User not found`);
|
||||
}
|
||||
const url = await this.storage.uploadFile(`${id}-avatar`, avatar);
|
||||
const url = await this.storage.uploadFile(`${user.id}-avatar`, avatar);
|
||||
return this.prisma.user.update({
|
||||
where: { id },
|
||||
where: { id: user.id },
|
||||
data: { avatarUrl: url },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import type { User, Workspace } from '@prisma/client';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
import { applyUpdate, Doc } from 'yjs';
|
||||
|
||||
@@ -648,10 +647,10 @@ export class WorkspaceResolver {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
return (
|
||||
userWorkspace?.accepted &&
|
||||
(await this.permissions.grantPage(workspaceId, pageId))
|
||||
);
|
||||
if (!userWorkspace?.accepted) {
|
||||
throw new ForbiddenException('Permission denied');
|
||||
}
|
||||
return this.permissions.grantPage(workspaceId, pageId);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
|
||||
@@ -190,7 +190,7 @@ type Mutation {
|
||||
deleteBlob(workspaceId: String!, hash: String!): Boolean!
|
||||
|
||||
"""Upload user avatar"""
|
||||
uploadAvatar(id: String!, avatar: Upload!): UserType!
|
||||
uploadAvatar(avatar: Upload!): UserType!
|
||||
|
||||
"""Remove user avatar"""
|
||||
removeAvatar: RemoveAvatar!
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
import { ok } from 'node:assert';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { Transformer } from '@napi-rs/image';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { hashSync } from '@node-rs/argon2';
|
||||
import { User } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import { Express } from 'express';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import { PrismaService } from '../prisma/service';
|
||||
|
||||
const gql = '/graphql';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
}>;
|
||||
|
||||
class FakePrisma {
|
||||
fakeUser: User = {
|
||||
id: randomUUID(),
|
||||
name: 'Alex Yang',
|
||||
avatarUrl: '',
|
||||
email: 'alex.yang@example.org',
|
||||
password: hashSync('123456'),
|
||||
emailVerified: new Date(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
get user() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const prisma = this;
|
||||
return {
|
||||
async findFirst() {
|
||||
return prisma.fakeUser;
|
||||
},
|
||||
async findUnique() {
|
||||
return this.findFirst();
|
||||
},
|
||||
async update() {
|
||||
return this.findFirst();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test.beforeEach(async t => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
})
|
||||
.overrideProvider(PrismaService)
|
||||
.useClass(FakePrisma)
|
||||
.compile();
|
||||
t.context.app = module.createNestApplication({
|
||||
cors: true,
|
||||
bodyParser: true,
|
||||
});
|
||||
t.context.app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await t.context.app.init();
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should init app', async t => {
|
||||
t.is(typeof t.context.app, 'object');
|
||||
await request(t.context.app.getHttpServer())
|
||||
.post(gql)
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
error
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
const { token } = await createToken(t.context.app);
|
||||
|
||||
await request(t.context.app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
__typename
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect(res => {
|
||||
t.is(res.body.data.__typename, 'Query');
|
||||
});
|
||||
});
|
||||
|
||||
test('should find default user', async t => {
|
||||
const { token } = await createToken(t.context.app);
|
||||
await request(t.context.app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
user(email: "alex.yang@example.org") {
|
||||
email
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect(res => {
|
||||
t.is(res.body.data.user.email, 'alex.yang@example.org');
|
||||
});
|
||||
});
|
||||
|
||||
test('should be able to upload avatar and remove it', async t => {
|
||||
const { token, id } = await createToken(t.context.app);
|
||||
const png = await Transformer.fromRgbaPixels(
|
||||
Buffer.alloc(400 * 400 * 4).fill(255),
|
||||
400,
|
||||
400
|
||||
).png();
|
||||
|
||||
await request(t.context.app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.field(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
name: 'uploadAvatar',
|
||||
query: `mutation uploadAvatar($id: String!, $avatar: Upload!) {
|
||||
uploadAvatar(id: $id, avatar: $avatar) {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
email
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id, avatar: null },
|
||||
})
|
||||
)
|
||||
.field('map', JSON.stringify({ '0': ['variables.avatar'] }))
|
||||
.attach('0', png, 'avatar.png')
|
||||
.expect(200)
|
||||
.expect(res => {
|
||||
t.is(res.body.data.uploadAvatar.id, id);
|
||||
});
|
||||
|
||||
await request(t.context.app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation removeAvatar {
|
||||
removeAvatar {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
email
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect(res => {
|
||||
t.is(res.body.data.removeAvatar.avatarUrl, '');
|
||||
});
|
||||
});
|
||||
|
||||
async function createToken(app: INestApplication<Express>): Promise<{
|
||||
id: string;
|
||||
token: string;
|
||||
}> {
|
||||
let token;
|
||||
let id;
|
||||
await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
signIn(email: "alex.yang@example.org", password: "123456") {
|
||||
id
|
||||
token {
|
||||
token
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect(res => {
|
||||
id = res.body.data.signIn.id;
|
||||
ok(
|
||||
typeof res.body.data.signIn.token.token === 'string',
|
||||
'res.body.data.signIn.token.token is not a string'
|
||||
);
|
||||
token = res.body.data.signIn.token.token;
|
||||
});
|
||||
return { token: token!, id: id! };
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
// @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 {
|
||||
changeEmail,
|
||||
createWorkspace,
|
||||
getCurrentMailMessageCount,
|
||||
getLatestMailMessage,
|
||||
sendChangeEmail,
|
||||
sendVerifyChangeEmail,
|
||||
signUp,
|
||||
} from './utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
client: PrismaClient;
|
||||
auth: AuthService;
|
||||
mail: MailService;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
const client = new PrismaClient();
|
||||
t.context.client = client;
|
||||
await client.$connect();
|
||||
await client.user.deleteMany({});
|
||||
await client.snapshot.deleteMany({});
|
||||
await client.update.deleteMany({});
|
||||
await client.workspace.deleteMany({});
|
||||
await client.$disconnect();
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
const app = module.createNestApplication();
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
|
||||
const auth = module.get(AuthService);
|
||||
const mail = module.get(MailService);
|
||||
t.context.app = app;
|
||||
t.context.auth = auth;
|
||||
t.context.mail = mail;
|
||||
});
|
||||
|
||||
test.afterEach(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('change email', async t => {
|
||||
const { mail, app } = t.context;
|
||||
if (mail.hasConfigured()) {
|
||||
const u1Email = 'u1@affine.pro';
|
||||
const u2Email = 'u2@affine.pro';
|
||||
const tokenRegex = /token=3D([^"&\s]+)/;
|
||||
|
||||
const u1 = await signUp(app, 'u1', u1Email, '1');
|
||||
|
||||
await createWorkspace(app, u1.token.token);
|
||||
|
||||
const primitiveMailCount = await getCurrentMailMessageCount();
|
||||
|
||||
await sendChangeEmail(app, u1.token.token, u1Email, 'affine.pro');
|
||||
|
||||
const afterSendChangeMailCount = await getCurrentMailMessageCount();
|
||||
t.is(
|
||||
primitiveMailCount + 1,
|
||||
afterSendChangeMailCount,
|
||||
'failed to send change email'
|
||||
);
|
||||
const changeEmailContent = await getLatestMailMessage();
|
||||
|
||||
const changeTokenMatch = changeEmailContent.Content.Body.match(tokenRegex);
|
||||
const changeEmailToken = changeTokenMatch
|
||||
? decodeURIComponent(changeTokenMatch[1].replace(/=3D/g, '='))
|
||||
: null;
|
||||
|
||||
t.not(
|
||||
changeEmailToken,
|
||||
null,
|
||||
'fail to get change email token from email content'
|
||||
);
|
||||
|
||||
await sendVerifyChangeEmail(
|
||||
app,
|
||||
u1.token.token,
|
||||
changeEmailToken as string,
|
||||
u2Email,
|
||||
'affine.pro'
|
||||
);
|
||||
|
||||
const afterSendVerifyMailCount = await getCurrentMailMessageCount();
|
||||
|
||||
t.is(
|
||||
afterSendChangeMailCount + 1,
|
||||
afterSendVerifyMailCount,
|
||||
'failed to send verify email'
|
||||
);
|
||||
const verifyEmailContent = await getLatestMailMessage();
|
||||
|
||||
const verifyTokenMatch = verifyEmailContent.Content.Body.match(tokenRegex);
|
||||
const verifyEmailToken = verifyTokenMatch
|
||||
? decodeURIComponent(verifyTokenMatch[1].replace(/=3D/g, '='))
|
||||
: null;
|
||||
|
||||
t.not(
|
||||
verifyEmailToken,
|
||||
null,
|
||||
'fail to get verify change email token from email content'
|
||||
);
|
||||
|
||||
await changeEmail(app, u1.token.token, verifyEmailToken as string);
|
||||
|
||||
const afterNotificationMailCount = await getCurrentMailMessageCount();
|
||||
|
||||
t.is(
|
||||
afterSendVerifyMailCount + 1,
|
||||
afterNotificationMailCount,
|
||||
'failed to send notification email'
|
||||
);
|
||||
}
|
||||
t.pass();
|
||||
});
|
||||
@@ -1,178 +0,0 @@
|
||||
/// <reference types="../global.d.ts" />
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
|
||||
import { ConfigModule } from '../config';
|
||||
import { GqlModule } from '../graphql.module';
|
||||
import { MetricsModule } from '../metrics';
|
||||
import { AuthModule } from '../modules/auth';
|
||||
import { AuthResolver } from '../modules/auth/resolver';
|
||||
import { AuthService } from '../modules/auth/service';
|
||||
import { PrismaModule } from '../prisma';
|
||||
import { RateLimiterModule } from '../throttler';
|
||||
|
||||
let authService: AuthService;
|
||||
let authResolver: AuthResolver;
|
||||
let module: TestingModule;
|
||||
|
||||
// cleanup database before each test
|
||||
test.beforeEach(async () => {
|
||||
const client = new PrismaClient();
|
||||
await client.$connect();
|
||||
await client.user.deleteMany({});
|
||||
await client.$disconnect();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
auth: {
|
||||
accessTokenExpiresIn: 1,
|
||||
refreshTokenExpiresIn: 1,
|
||||
leeway: 1,
|
||||
},
|
||||
host: 'example.org',
|
||||
https: true,
|
||||
}),
|
||||
PrismaModule,
|
||||
GqlModule,
|
||||
AuthModule,
|
||||
MetricsModule,
|
||||
RateLimiterModule,
|
||||
],
|
||||
}).compile();
|
||||
authService = module.get(AuthService);
|
||||
authResolver = module.get(AuthResolver);
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('should be able to register and signIn', async t => {
|
||||
await authService.signUp('Alex Yang', 'alexyang@example.org', '123456');
|
||||
await authService.signIn('alexyang@example.org', '123456');
|
||||
t.pass();
|
||||
});
|
||||
|
||||
test('should be able to verify', async t => {
|
||||
await authService.signUp('Alex Yang', 'alexyang@example.org', '123456');
|
||||
await authService.signIn('alexyang@example.org', '123456');
|
||||
const date = new Date();
|
||||
|
||||
const user = {
|
||||
id: '1',
|
||||
name: 'Alex Yang',
|
||||
email: 'alexyang@example.org',
|
||||
emailVerified: date,
|
||||
createdAt: date,
|
||||
avatarUrl: '',
|
||||
};
|
||||
{
|
||||
const token = await authService.sign(user);
|
||||
const claim = await authService.verify(token);
|
||||
t.is(claim.id, '1');
|
||||
t.is(claim.name, 'Alex Yang');
|
||||
t.is(claim.email, 'alexyang@example.org');
|
||||
t.is(claim.emailVerified?.toISOString(), date.toISOString());
|
||||
t.is(claim.createdAt.toISOString(), date.toISOString());
|
||||
}
|
||||
{
|
||||
const token = await authService.refresh(user);
|
||||
const claim = await authService.verify(token);
|
||||
t.is(claim.id, '1');
|
||||
t.is(claim.name, 'Alex Yang');
|
||||
t.is(claim.email, 'alexyang@example.org');
|
||||
t.is(claim.emailVerified?.toISOString(), date.toISOString());
|
||||
t.is(claim.createdAt.toISOString(), date.toISOString());
|
||||
}
|
||||
});
|
||||
|
||||
test('should not be able to return token if user is invalid', async t => {
|
||||
const date = new Date();
|
||||
const user = {
|
||||
id: '1',
|
||||
name: 'Alex Yang',
|
||||
email: 'alexyang@example.org',
|
||||
emailVerified: date,
|
||||
createdAt: date,
|
||||
avatarUrl: '',
|
||||
};
|
||||
const anotherUser = {
|
||||
id: '2',
|
||||
name: 'Alex Yang 2',
|
||||
email: 'alexyang@example.org',
|
||||
emailVerified: date,
|
||||
createdAt: date,
|
||||
avatarUrl: '',
|
||||
};
|
||||
await t.throwsAsync(
|
||||
authResolver.token(
|
||||
{
|
||||
req: {
|
||||
headers: {
|
||||
referer: 'https://example.org',
|
||||
host: 'example.org',
|
||||
},
|
||||
} as any,
|
||||
},
|
||||
user,
|
||||
anotherUser
|
||||
),
|
||||
{
|
||||
message: 'Invalid user',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('should not return sessionToken if request headers is invalid', async t => {
|
||||
const date = new Date();
|
||||
const user = {
|
||||
id: '1',
|
||||
name: 'Alex Yang',
|
||||
email: 'alexyang@example.org',
|
||||
emailVerified: date,
|
||||
createdAt: date,
|
||||
avatarUrl: '',
|
||||
};
|
||||
const result = await authResolver.token(
|
||||
{
|
||||
req: {
|
||||
headers: {},
|
||||
} as any,
|
||||
},
|
||||
user,
|
||||
user
|
||||
);
|
||||
t.is(result.sessionToken, undefined);
|
||||
});
|
||||
|
||||
test('should return valid sessionToken if request headers valid', async t => {
|
||||
const date = new Date();
|
||||
const user = {
|
||||
id: '1',
|
||||
name: 'Alex Yang',
|
||||
email: 'alexyang@example.org',
|
||||
emailVerified: date,
|
||||
createdAt: date,
|
||||
avatarUrl: '',
|
||||
};
|
||||
const result = await authResolver.token(
|
||||
{
|
||||
req: {
|
||||
headers: {
|
||||
referer: 'https://example.org/open-app/test',
|
||||
host: 'example.org',
|
||||
},
|
||||
cookies: {
|
||||
'next-auth.session-token': '123456',
|
||||
},
|
||||
} as any,
|
||||
},
|
||||
user,
|
||||
user
|
||||
);
|
||||
t.is(result.sessionToken, '123456');
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import test from 'ava';
|
||||
|
||||
import { Config, ConfigModule } from '../config';
|
||||
|
||||
let config: Config;
|
||||
let module: TestingModule;
|
||||
test.beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [ConfigModule.forRoot()],
|
||||
}).compile();
|
||||
config = module.get(Config);
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('should be able to get config', t => {
|
||||
t.true(typeof config.host === 'string');
|
||||
t.is(config.env, 'test');
|
||||
});
|
||||
|
||||
test('should be able to override config', async t => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
host: 'testing',
|
||||
}),
|
||||
],
|
||||
}).compile();
|
||||
const config = module.get(Config);
|
||||
|
||||
t.is(config.host, 'testing');
|
||||
});
|
||||
@@ -1,160 +0,0 @@
|
||||
import { mock } from 'node:test';
|
||||
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import test from 'ava';
|
||||
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();
|
||||
};
|
||||
|
||||
let app: INestApplication;
|
||||
let m: TestingModule;
|
||||
let timer: Sinon.SinonFakeTimers;
|
||||
|
||||
// cleanup database before each test
|
||||
test.beforeEach(async () => {
|
||||
timer = Sinon.useFakeTimers({
|
||||
toFake: ['setInterval'],
|
||||
});
|
||||
await flushDB();
|
||||
m = await createModule();
|
||||
app = m.createNestApplication();
|
||||
app.enableShutdownHooks();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
await app.close();
|
||||
await m.close();
|
||||
timer.restore();
|
||||
});
|
||||
|
||||
test('should setup update poll interval', async t => {
|
||||
register.clear();
|
||||
const m = await createModule();
|
||||
const manager = m.get(DocManager);
|
||||
const fake = mock.method(manager, 'setup');
|
||||
|
||||
await m.createNestApplication().init();
|
||||
|
||||
t.is(fake.mock.callCount(), 1);
|
||||
// @ts-expect-error private member
|
||||
t.truthy(manager.job);
|
||||
});
|
||||
|
||||
test('should be able to stop poll', async t => {
|
||||
const manager = m.get(DocManager);
|
||||
const fake = mock.method(manager, 'destroy');
|
||||
|
||||
await app.close();
|
||||
|
||||
t.is(fake.mock.callCount(), 1);
|
||||
// @ts-expect-error private member
|
||||
t.is(manager.job, null);
|
||||
});
|
||||
|
||||
test('should poll when intervel due', async t => {
|
||||
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);
|
||||
t.is(fake.mock.callCount(), 1);
|
||||
|
||||
// busy
|
||||
timer.tick(interval);
|
||||
// @ts-expect-error private member
|
||||
t.is(manager.busy, true);
|
||||
t.is(fake.mock.callCount(), 1);
|
||||
|
||||
resolve();
|
||||
await timer.tickAsync(1);
|
||||
|
||||
// @ts-expect-error private member
|
||||
t.is(manager.busy, false);
|
||||
timer.tick(interval);
|
||||
t.is(fake.mock.callCount(), 2);
|
||||
});
|
||||
|
||||
test('should merge update when intervel due', async t => {
|
||||
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();
|
||||
|
||||
t.deepEqual(
|
||||
(await manager.getLatestUpdate(ws.id, '1'))?.toString('hex'),
|
||||
Buffer.from(update.buffer).toString('hex')
|
||||
);
|
||||
|
||||
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();
|
||||
|
||||
t.deepEqual(
|
||||
(await manager.getLatestUpdate(ws.id, '1'))?.toString('hex'),
|
||||
Buffer.from(encodeStateAsUpdate(doc)).toString('hex')
|
||||
);
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Controller, Get, INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import test from 'ava';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import { ExceptionLogger } from '../middleware/exception-logger';
|
||||
import { PrismaService } from '../prisma';
|
||||
|
||||
const gql = '/graphql';
|
||||
const rest = '/rest';
|
||||
|
||||
let app: INestApplication;
|
||||
|
||||
class FakePrisma {
|
||||
get workspace() {
|
||||
return {
|
||||
async findUnique() {
|
||||
throw Error('exception from graphql');
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('rest')
|
||||
export class MockController {
|
||||
@Get()
|
||||
test(): string {
|
||||
throw new Error('exception from rest api');
|
||||
}
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
controllers: [MockController],
|
||||
})
|
||||
.overrideProvider(PrismaService)
|
||||
.useClass(FakePrisma)
|
||||
.compile();
|
||||
app = module.createNestApplication({
|
||||
cors: true,
|
||||
bodyParser: true,
|
||||
});
|
||||
app.useGlobalFilters(new ExceptionLogger());
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('should get response from graphql', async t => {
|
||||
const id = 'workspace';
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.send({
|
||||
name: 'getPublicWorkspace',
|
||||
query: `
|
||||
query getPublicWorkspace($id: String!) {
|
||||
publicWorkspace(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
t.is(response.status, 200);
|
||||
t.is(response.body.errors[0].message, 'exception from graphql');
|
||||
});
|
||||
|
||||
test('should get response from rest api', async t => {
|
||||
const response = await request(app.getHttpServer()).get(rest);
|
||||
|
||||
t.is(response.status, 500);
|
||||
t.is(response.body.error, 'exception from rest api');
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
/// <reference types="../global.d.ts" />
|
||||
// This test case is for testing the mailer service.
|
||||
// Please use local SMTP server for testing.
|
||||
// See: https://github.com/mailhog/MailHog
|
||||
import {
|
||||
getCurrentMailMessageCount,
|
||||
getLatestMailMessage,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
|
||||
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';
|
||||
import { RateLimiterModule } from '../throttler';
|
||||
|
||||
let auth: AuthService;
|
||||
let module: TestingModule;
|
||||
let skip = false;
|
||||
|
||||
test.before(async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8025/api/v2/messages');
|
||||
if (!response.ok && !process.env.CI) {
|
||||
console.warn('local mail not found, skip the mailer.e2e.ts');
|
||||
skip = true;
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.cause as any)?.code === 'ECONNREFUSED' &&
|
||||
!process.env.CI
|
||||
) {
|
||||
console.warn('local mail not found, skip the mailer.e2e.ts');
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// cleanup database before each test
|
||||
test.beforeEach(async () => {
|
||||
const client = new PrismaClient();
|
||||
await client.$connect();
|
||||
await client.user.deleteMany({});
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
auth: {
|
||||
accessTokenExpiresIn: 1,
|
||||
refreshTokenExpiresIn: 1,
|
||||
leeway: 1,
|
||||
},
|
||||
}),
|
||||
PrismaModule,
|
||||
GqlModule,
|
||||
AuthModule,
|
||||
MetricsModule,
|
||||
RateLimiterModule,
|
||||
],
|
||||
}).compile();
|
||||
auth = module.get(AuthService);
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('should include callbackUrl in sending email', async t => {
|
||||
if (skip) {
|
||||
return t.pass();
|
||||
}
|
||||
// await auth.signUp('Alex Yang', 'alexyang@example.org', '123456');
|
||||
for (const fn of [
|
||||
'sendSetPasswordEmail',
|
||||
'sendChangeEmail',
|
||||
'sendChangePasswordEmail',
|
||||
'sendVerifyChangeEmail',
|
||||
] as const) {
|
||||
const prev = await getCurrentMailMessageCount();
|
||||
await auth[fn]('alexyang@example.org', 'https://test.com/callback');
|
||||
const current = await getCurrentMailMessageCount();
|
||||
const mail = await getLatestMailMessage();
|
||||
t.regex(
|
||||
mail.Content.Body,
|
||||
/https:\/\/test.com\/callback/,
|
||||
`should include callbackUrl when calling ${fn}`
|
||||
);
|
||||
t.is(current, prev + 1, `calling ${fn}`);
|
||||
}
|
||||
return;
|
||||
});
|
||||
@@ -1,156 +0,0 @@
|
||||
import { ok } from 'node:assert';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { hashSync } from '@node-rs/argon2';
|
||||
import { User } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
// @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 { PrismaService } from '../prisma';
|
||||
import { createWorkspace, getInviteInfo, inviteUser, signUp } from './utils';
|
||||
|
||||
let app: INestApplication;
|
||||
|
||||
let mail: MailService;
|
||||
|
||||
const FakePrisma = {
|
||||
fakeUser: {
|
||||
id: randomUUID(),
|
||||
name: 'Alex Yang',
|
||||
avatarUrl: '',
|
||||
email: 'alex.yang@example.org',
|
||||
password: hashSync('123456'),
|
||||
emailVerified: new Date(),
|
||||
createdAt: new Date(),
|
||||
} satisfies User,
|
||||
|
||||
get user() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const prisma = this;
|
||||
return {
|
||||
async findFirst() {
|
||||
return null;
|
||||
},
|
||||
async create({ data }: any) {
|
||||
return {
|
||||
...prisma.fakeUser,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
async findUnique() {
|
||||
return prisma.fakeUser;
|
||||
},
|
||||
};
|
||||
},
|
||||
get workspace() {
|
||||
return {
|
||||
id: randomUUID(),
|
||||
async create({ data }: any) {
|
||||
return {
|
||||
id: this.id,
|
||||
public: data.public ?? false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
snapshot: {
|
||||
id: randomUUID(),
|
||||
async create() {},
|
||||
async findFirstOrThrow() {
|
||||
return { id: this.id, blob: Buffer.from([0, 0]) };
|
||||
},
|
||||
},
|
||||
get userWorkspacePermission() {
|
||||
return {
|
||||
id: randomUUID(),
|
||||
prisma: this,
|
||||
async count() {
|
||||
return 1;
|
||||
},
|
||||
async create() {
|
||||
return { id: this.id };
|
||||
},
|
||||
async findUniqueOrThrow() {
|
||||
return { id: this.id, workspaceId: this.prisma.workspace.id };
|
||||
},
|
||||
async findFirst() {
|
||||
return { id: this.id };
|
||||
},
|
||||
async findFirstOrThrow() {
|
||||
return { id: this.id, user: this.prisma.fakeUser };
|
||||
},
|
||||
async userWorkspacePermission() {
|
||||
return {
|
||||
id: randomUUID(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
test.beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
})
|
||||
.overrideProvider(PrismaService)
|
||||
.useValue(FakePrisma)
|
||||
.compile();
|
||||
app = module.createNestApplication();
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
|
||||
mail = module.get(MailService);
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('should send invite email', async t => {
|
||||
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');
|
||||
}
|
||||
t.pass();
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import test from 'ava';
|
||||
import { register } from 'prom-client';
|
||||
|
||||
import { MetricsModule } from '../metrics';
|
||||
import { Metrics } from '../metrics/metrics';
|
||||
import { PrismaModule } from '../prisma';
|
||||
|
||||
let metrics: Metrics;
|
||||
let module: TestingModule;
|
||||
|
||||
test.beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [MetricsModule, PrismaModule],
|
||||
}).compile();
|
||||
|
||||
metrics = module.get(Metrics);
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('should be able to increment counter', async t => {
|
||||
metrics.socketIOEventCounter(1, { event: 'client-handshake' });
|
||||
const socketIOCounterMetric =
|
||||
await register.getSingleMetric('socket_io_counter');
|
||||
t.truthy(socketIOCounterMetric);
|
||||
|
||||
t.truthy(
|
||||
JSON.stringify((await socketIOCounterMetric!.get()).values) ===
|
||||
'[{"value":1,"labels":{"event":"client-handshake"}}]'
|
||||
);
|
||||
t.pass();
|
||||
});
|
||||
|
||||
test('should be able to timer', async t => {
|
||||
let minimum: number;
|
||||
{
|
||||
const endTimer = metrics.socketIOEventTimer({ event: 'client-handshake' });
|
||||
const a = performance.now();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
const b = performance.now();
|
||||
minimum = b - a;
|
||||
endTimer();
|
||||
}
|
||||
|
||||
let maximum: number;
|
||||
{
|
||||
const a = performance.now();
|
||||
const endTimer = metrics.socketIOEventTimer({ event: 'client-handshake' });
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
endTimer();
|
||||
const b = performance.now();
|
||||
maximum = b - a;
|
||||
}
|
||||
|
||||
const socketIOTimerMetric = register.getSingleMetric('socket_io_timer');
|
||||
t.truthy(socketIOTimerMetric);
|
||||
|
||||
const observations = (await socketIOTimerMetric!.get()).values;
|
||||
|
||||
for (const observation of observations) {
|
||||
if (
|
||||
observation.labels.event === 'client-handshake' &&
|
||||
'quantile' in observation.labels
|
||||
) {
|
||||
t.truthy(
|
||||
observation.value >= minimum / 1000,
|
||||
'observation.value should be greater than minimum'
|
||||
);
|
||||
t.truthy(
|
||||
observation.value <= maximum / 1000,
|
||||
'observation.value should be less than maximum'
|
||||
);
|
||||
}
|
||||
}
|
||||
t.pass();
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
/// <reference types="../global.d.ts" />
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import ava, { TestFn } from 'ava';
|
||||
|
||||
import { ConfigModule } from '../config';
|
||||
import { SessionModule, SessionService } from '../session';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
session: SessionService;
|
||||
app: TestingModule;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
redis: {
|
||||
enabled: false,
|
||||
},
|
||||
}),
|
||||
SessionModule,
|
||||
],
|
||||
}).compile();
|
||||
const session = module.get(SessionService);
|
||||
t.context.app = module;
|
||||
t.context.session = session;
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should be able to set session', async t => {
|
||||
const { session } = t.context;
|
||||
await session.set('test', 'value');
|
||||
t.is(await session.get('test'), 'value');
|
||||
});
|
||||
|
||||
test('should be expired by ttl', async t => {
|
||||
const { session } = t.context;
|
||||
await session.set('test', 'value', 100);
|
||||
t.is(await session.get('test'), 'value');
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
t.is(await session.get('test'), undefined);
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
// @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';
|
||||
|
||||
let app: INestApplication;
|
||||
|
||||
// cleanup database before each test
|
||||
test.beforeEach(async () => {
|
||||
const client = new PrismaClient();
|
||||
await client.$connect();
|
||||
await client.user.deleteMany({});
|
||||
await client.$disconnect();
|
||||
});
|
||||
|
||||
test.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();
|
||||
});
|
||||
|
||||
test.afterEach.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');
|
||||
});
|
||||
|
||||
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);
|
||||
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');
|
||||
t.true(currUser.hasPassword, 'currUser.hasPassword is not valid');
|
||||
});
|
||||
|
||||
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);
|
||||
await t.throwsAsync(() => currentUser(app, user.token.token));
|
||||
t.pass();
|
||||
});
|
||||
@@ -1,598 +0,0 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { hashSync } from '@node-rs/argon2';
|
||||
import { PrismaClient, User } from '@prisma/client';
|
||||
import request from 'supertest';
|
||||
|
||||
import type { TokenType } from '../modules/auth';
|
||||
import type { UserType } from '../modules/users';
|
||||
import type { InvitationType, WorkspaceType } from '../modules/workspaces';
|
||||
|
||||
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,
|
||||
token { token }
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.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,
|
||||
skip = 0,
|
||||
take = 8
|
||||
): Promise<WorkspaceType> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
workspace(id: "${workspaceId}") {
|
||||
id, members(skip: ${skip}, take: ${take}) { id, name, email, permission, inviteId }
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.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,
|
||||
sendAcceptMail = false
|
||||
): 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}", sendAcceptMail: ${sendAcceptMail})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.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,
|
||||
sendLeaveMail = false
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
leaveWorkspace(workspaceId: "${workspaceId}", workspaceName: "test workspace", sendLeaveMail: ${sendLeaveMail})
|
||||
}
|
||||
`,
|
||||
})
|
||||
.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 collectBlobSizes(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string
|
||||
): Promise<number> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
collectBlobSizes(workspaceId: "${workspaceId}") {
|
||||
size
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.collectBlobSizes.size;
|
||||
}
|
||||
|
||||
async function collectAllBlobSizes(
|
||||
app: INestApplication,
|
||||
token: string
|
||||
): Promise<number> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
collectAllBlobSizes {
|
||||
size
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.collectAllBlobSizes.size;
|
||||
}
|
||||
|
||||
async function checkBlobSize(
|
||||
app: INestApplication,
|
||||
token: string,
|
||||
workspaceId: string,
|
||||
size: number
|
||||
): Promise<number> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `query checkBlobSize($workspaceId: String!, $size: Float!) {
|
||||
checkBlobSize(workspaceId: $workspaceId, size: $size) {
|
||||
size
|
||||
}
|
||||
}`,
|
||||
variables: { workspaceId, size },
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.checkBlobSize.size;
|
||||
}
|
||||
|
||||
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 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.getInviteInfo;
|
||||
}
|
||||
|
||||
async function sendChangeEmail(
|
||||
app: INestApplication,
|
||||
userToken: string,
|
||||
email: string,
|
||||
callbackUrl: string
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(userToken, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
sendChangeEmail(email: "${email}", callbackUrl: "${callbackUrl}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
return res.body.data.sendChangeEmail;
|
||||
}
|
||||
|
||||
async function sendVerifyChangeEmail(
|
||||
app: INestApplication,
|
||||
userToken: string,
|
||||
token: string,
|
||||
email: string,
|
||||
callbackUrl: string
|
||||
): Promise<boolean> {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(userToken, { type: 'bearer' })
|
||||
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
sendVerifyChangeEmail(token:"${token}", email: "${email}", callbackUrl: "${callbackUrl}")
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
return res.body.data.sendVerifyChangeEmail;
|
||||
}
|
||||
|
||||
async function changeEmail(
|
||||
app: INestApplication,
|
||||
userToken: string,
|
||||
token: string
|
||||
): Promise<UserType & { token: TokenType }> {
|
||||
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}") {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
email
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200);
|
||||
return res.body.data.changeEmail;
|
||||
}
|
||||
|
||||
export class FakePrisma {
|
||||
fakeUser: User = {
|
||||
id: randomUUID(),
|
||||
name: 'Alex Yang',
|
||||
avatarUrl: '',
|
||||
email: 'alex.yang@example.org',
|
||||
password: hashSync('123456'),
|
||||
emailVerified: new Date(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
get user() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const prisma = this;
|
||||
return {
|
||||
async findFirst() {
|
||||
return prisma.fakeUser;
|
||||
},
|
||||
async findUnique() {
|
||||
return this.findFirst();
|
||||
},
|
||||
async update() {
|
||||
return this.findFirst();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
acceptInvite,
|
||||
acceptInviteById,
|
||||
changeEmail,
|
||||
checkBlobSize,
|
||||
collectAllBlobSizes,
|
||||
collectBlobSizes,
|
||||
createWorkspace,
|
||||
currentUser,
|
||||
flushDB,
|
||||
getInviteInfo,
|
||||
getPublicWorkspace,
|
||||
getWorkspace,
|
||||
inviteUser,
|
||||
leaveWorkspace,
|
||||
listBlobs,
|
||||
revokePage,
|
||||
revokeUser,
|
||||
sendChangeEmail,
|
||||
sendVerifyChangeEmail,
|
||||
setBlob,
|
||||
sharePage,
|
||||
signUp,
|
||||
updateWorkspace,
|
||||
};
|
||||
@@ -1,146 +0,0 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import {
|
||||
checkBlobSize,
|
||||
collectAllBlobSizes,
|
||||
collectBlobSizes,
|
||||
createWorkspace,
|
||||
listBlobs,
|
||||
setBlob,
|
||||
signUp,
|
||||
} from './utils';
|
||||
|
||||
let app: INestApplication;
|
||||
|
||||
const client = new PrismaClient();
|
||||
|
||||
// cleanup database before each test
|
||||
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();
|
||||
});
|
||||
|
||||
test.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();
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('should set blobs', async t => {
|
||||
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||
|
||||
const workspace = await createWorkspace(app, u1.token.token);
|
||||
|
||||
const buffer1 = Buffer.from([0, 0]);
|
||||
const hash1 = await setBlob(app, u1.token.token, workspace.id, buffer1);
|
||||
const buffer2 = Buffer.from([0, 1]);
|
||||
const hash2 = await setBlob(app, u1.token.token, 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' })
|
||||
.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' })
|
||||
.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');
|
||||
|
||||
const workspace = await createWorkspace(app, u1.token.token);
|
||||
const blobs = await listBlobs(app, u1.token.token, 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 buffer2 = Buffer.from([0, 1]);
|
||||
const hash2 = await setBlob(app, u1.token.token, workspace.id, buffer2);
|
||||
|
||||
const ret = await listBlobs(app, u1.token.token, workspace.id);
|
||||
t.is(ret.length, 2, 'failed to list blobs');
|
||||
t.is(ret[0], hash1, 'failed to list blobs');
|
||||
t.is(ret[1], hash2, 'failed to list blobs');
|
||||
});
|
||||
|
||||
test('should calc blobs size', async t => {
|
||||
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
|
||||
|
||||
const workspace = await createWorkspace(app, u1.token.token);
|
||||
|
||||
const buffer1 = Buffer.from([0, 0]);
|
||||
await setBlob(app, u1.token.token, workspace.id, buffer1);
|
||||
const buffer2 = Buffer.from([0, 1]);
|
||||
await setBlob(app, u1.token.token, workspace.id, buffer2);
|
||||
|
||||
const size = await collectBlobSizes(app, u1.token.token, 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');
|
||||
|
||||
const workspace1 = await createWorkspace(app, u1.token.token);
|
||||
|
||||
const buffer1 = Buffer.from([0, 0]);
|
||||
await setBlob(app, u1.token.token, workspace1.id, buffer1);
|
||||
const buffer2 = Buffer.from([0, 1]);
|
||||
await setBlob(app, u1.token.token, workspace1.id, buffer2);
|
||||
|
||||
const workspace2 = await createWorkspace(app, u1.token.token);
|
||||
|
||||
const buffer3 = Buffer.from([0, 0]);
|
||||
await setBlob(app, u1.token.token, workspace2.id, buffer3);
|
||||
const buffer4 = Buffer.from([0, 1]);
|
||||
await setBlob(app, u1.token.token, workspace2.id, buffer4);
|
||||
|
||||
const size = await collectAllBlobSizes(app, u1.token.token);
|
||||
t.is(size, 8, 'failed to collect all blob sizes');
|
||||
|
||||
const size1 = await checkBlobSize(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace1.id,
|
||||
10 * 1024 * 1024 * 1024 - 8
|
||||
);
|
||||
t.is(size1, 0, 'failed to check blob size');
|
||||
|
||||
const size2 = await checkBlobSize(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace1.id,
|
||||
10 * 1024 * 1024 * 1024 - 7
|
||||
);
|
||||
t.is(size2, -1, 'failed to check blob size');
|
||||
});
|
||||
@@ -1,282 +0,0 @@
|
||||
import {
|
||||
getCurrentMailMessageCount,
|
||||
getLatestMailMessage,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
// @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';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
client: PrismaClient;
|
||||
auth: AuthService;
|
||||
mail: MailService;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
const client = new PrismaClient();
|
||||
t.context.client = client;
|
||||
await client.$connect();
|
||||
await client.user.deleteMany({});
|
||||
await client.snapshot.deleteMany({});
|
||||
await client.update.deleteMany({});
|
||||
await client.workspace.deleteMany({});
|
||||
await client.$disconnect();
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
const app = module.createNestApplication();
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
|
||||
const auth = module.get(AuthService);
|
||||
const mail = module.get(MailService);
|
||||
t.context.app = app;
|
||||
t.context.auth = auth;
|
||||
t.context.mail = mail;
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
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 workspace = await createWorkspace(app, u1.token.token);
|
||||
|
||||
const invite = await inviteUser(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id,
|
||||
u2.email,
|
||||
'Admin'
|
||||
);
|
||||
t.truthy(invite, 'failed to invite user');
|
||||
});
|
||||
|
||||
test('should accept an invite', 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 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);
|
||||
t.is(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);
|
||||
t.not(currMember, undefined, 'failed to invite user');
|
||||
t.is(currMember!.id, u2.id, 'failed to invite user');
|
||||
t.true(!currMember!.accepted, 'failed to invite user');
|
||||
t.pass();
|
||||
});
|
||||
|
||||
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 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);
|
||||
|
||||
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 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);
|
||||
t.is(currWorkspace.members.length, 2, 'failed to invite user');
|
||||
|
||||
const revoke = await revokeUser(app, u1.token.token, workspace.id, u2.id);
|
||||
t.true(revoke, 'failed to revoke user');
|
||||
});
|
||||
|
||||
test('should create user if not exist', async t => {
|
||||
const { app, auth } = t.context;
|
||||
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');
|
||||
t.not(user, undefined, 'failed to create user');
|
||||
t.is(user?.name, 'Unnamed', 'failed to create user');
|
||||
});
|
||||
|
||||
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 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);
|
||||
t.true(accept, 'failed to accept invite');
|
||||
|
||||
const invite1 = await inviteUser(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id,
|
||||
u2.email,
|
||||
'Admin'
|
||||
);
|
||||
|
||||
t.is(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);
|
||||
t.not(currMember, undefined, 'failed to invite user');
|
||||
t.is(currMember?.inviteId, invite, 'failed to check invite id');
|
||||
});
|
||||
|
||||
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 workspace = await createWorkspace(app, u1.token.token);
|
||||
const primitiveMailCount = await getCurrentMailMessageCount();
|
||||
|
||||
const invite = await inviteUser(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id,
|
||||
u2.email,
|
||||
'Admin',
|
||||
true
|
||||
);
|
||||
|
||||
const afterInviteMailCount = await getCurrentMailMessageCount();
|
||||
t.is(
|
||||
primitiveMailCount + 1,
|
||||
afterInviteMailCount,
|
||||
'failed to send invite email'
|
||||
);
|
||||
const inviteEmailContent = await getLatestMailMessage();
|
||||
|
||||
t.not(
|
||||
// @ts-expect-error Third part library type mismatch
|
||||
inviteEmailContent.To.find(item => {
|
||||
return item.Mailbox === 'production';
|
||||
}),
|
||||
undefined,
|
||||
'invite email address was incorrectly sent'
|
||||
);
|
||||
|
||||
const accept = await acceptInviteById(app, workspace.id, invite, true);
|
||||
t.true(accept, 'failed to accept invite');
|
||||
|
||||
const afterAcceptMailCount = await getCurrentMailMessageCount();
|
||||
t.is(
|
||||
afterInviteMailCount + 1,
|
||||
afterAcceptMailCount,
|
||||
'failed to send accepted email to owner'
|
||||
);
|
||||
const acceptEmailContent = await getLatestMailMessage();
|
||||
t.not(
|
||||
// @ts-expect-error Third part library type mismatch
|
||||
acceptEmailContent.To.find(item => {
|
||||
return item.Mailbox === 'u1';
|
||||
}),
|
||||
undefined,
|
||||
'accept email address was incorrectly sent'
|
||||
);
|
||||
|
||||
await leaveWorkspace(app, u2.token.token, workspace.id, true);
|
||||
|
||||
const afterLeaveMailCount = await getCurrentMailMessageCount();
|
||||
t.is(
|
||||
afterAcceptMailCount + 1,
|
||||
afterLeaveMailCount,
|
||||
'failed to send leave email to owner'
|
||||
);
|
||||
const leaveEmailContent = await getLatestMailMessage();
|
||||
t.not(
|
||||
// @ts-expect-error Third part library type mismatch
|
||||
leaveEmailContent.To.find(item => {
|
||||
return item.Mailbox === 'u1';
|
||||
}),
|
||||
undefined,
|
||||
'leave email address was incorrectly sent'
|
||||
);
|
||||
}
|
||||
t.pass();
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
const workspace = await createWorkspace(app, u1.token.token);
|
||||
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin');
|
||||
await inviteUser(app, u1.token.token, workspace.id, u3.email, 'Admin');
|
||||
|
||||
await acceptInvite(app, u2.token.token, workspace.id);
|
||||
await acceptInvite(app, u3.token.token, workspace.id);
|
||||
|
||||
const firstPageWorkspace = await getWorkspace(
|
||||
app,
|
||||
u1.token.token,
|
||||
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
|
||||
);
|
||||
t.is(secondPageWorkspace.members.length, 1, 'failed to check invite id');
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import { stub } from 'sinon';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import { UsersService } from '../modules/users';
|
||||
import { PermissionService } from '../modules/workspaces/permission';
|
||||
import { WorkspaceResolver } from '../modules/workspaces/resolver';
|
||||
import { PrismaService } from '../prisma';
|
||||
import { StorageProvide } from '../storage';
|
||||
import { FakePrisma } from './utils';
|
||||
|
||||
class FakePermission {
|
||||
async tryCheck() {
|
||||
return true;
|
||||
}
|
||||
async getWorkspaceOwner() {
|
||||
return {
|
||||
user: new FakePrisma().fakeUser,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const fakeUserService = {
|
||||
getStorageQuotaById: stub(),
|
||||
};
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
resolver: WorkspaceResolver;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
})
|
||||
.overrideProvider(PrismaService)
|
||||
.useValue({
|
||||
userWorkspacePermission: {
|
||||
async findMany() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
})
|
||||
.overrideProvider(PermissionService)
|
||||
.useClass(FakePermission)
|
||||
.overrideProvider(UsersService)
|
||||
.useValue(fakeUserService)
|
||||
.overrideProvider(StorageProvide)
|
||||
.useValue({
|
||||
blobsSize() {
|
||||
return 1024 * 10;
|
||||
},
|
||||
})
|
||||
.compile();
|
||||
t.context.app = module.createNestApplication();
|
||||
t.context.resolver = t.context.app.get(WorkspaceResolver);
|
||||
await t.context.app.init();
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should get blob size limit', async t => {
|
||||
const { resolver } = t.context;
|
||||
fakeUserService.getStorageQuotaById.returns(
|
||||
Promise.resolve(100 * 1024 * 1024 * 1024)
|
||||
);
|
||||
const res = await resolver.checkBlobSize(new FakePrisma().fakeUser, '', 100);
|
||||
t.not(res, false);
|
||||
// @ts-expect-error
|
||||
t.is(typeof res.size, 'number');
|
||||
fakeUserService.getStorageQuotaById.reset();
|
||||
});
|
||||
@@ -1,235 +0,0 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import {
|
||||
acceptInvite,
|
||||
createWorkspace,
|
||||
currentUser,
|
||||
getPublicWorkspace,
|
||||
getWorkspaceSharedPages,
|
||||
inviteUser,
|
||||
revokePage,
|
||||
sharePage,
|
||||
signUp,
|
||||
updateWorkspace,
|
||||
} from './utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
client: PrismaClient;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
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();
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
const app = module.createNestApplication();
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
t.context.client = client;
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
test.afterEach.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.skip('should be throttled at call signUp', async t => {
|
||||
const { app } = t.context;
|
||||
let token = '';
|
||||
for (let i = 0; i < 10; i++) {
|
||||
token = (await signUp(app, `u${i}`, `u${i}@affine.pro`, `${i}`)).token
|
||||
.token;
|
||||
// throttles are applied to each endpoint separately
|
||||
await currentUser(app, token);
|
||||
}
|
||||
await t.throwsAsync(() => signUp(app, 'u11', 'u11@affine.pro', '11'));
|
||||
await t.throwsAsync(() => currentUser(app, token));
|
||||
});
|
||||
|
||||
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);
|
||||
t.is(typeof workspace.id, 'string', 'workspace.id is not a string');
|
||||
});
|
||||
|
||||
test('should can 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);
|
||||
|
||||
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
|
||||
);
|
||||
t.false(isPrivate, 'failed to unpublish workspace');
|
||||
});
|
||||
|
||||
test('should can read published 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 t.throwsAsync(() => getPublicWorkspace(app, 'not_exists_ws'));
|
||||
await t.throwsAsync(() => getPublicWorkspace(app, workspace.id));
|
||||
|
||||
await updateWorkspace(app, user.token.token, workspace.id, true);
|
||||
|
||||
const publicWorkspace = await getPublicWorkspace(app, workspace.id);
|
||||
t.is(publicWorkspace.id, workspace.id, 'failed to get public workspace');
|
||||
});
|
||||
|
||||
test('should share a 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');
|
||||
|
||||
const workspace = await createWorkspace(app, u1.token.token);
|
||||
|
||||
const share = await sharePage(app, u1.token.token, workspace.id, 'page1');
|
||||
t.true(share, 'failed to share page');
|
||||
const pages = await getWorkspaceSharedPages(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id
|
||||
);
|
||||
t.is(pages.length, 1, 'failed to get shared pages');
|
||||
t.is(pages[0], 'page1', 'failed to get shared page: page1');
|
||||
|
||||
const msg1 = await sharePage(app, u2.token.token, workspace.id, 'page2');
|
||||
t.is(msg1, 'Permission denied', 'unauthorized user can share page');
|
||||
const msg2 = await revokePage(app, u2.token.token, 'not_exists_ws', 'page2');
|
||||
t.is(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');
|
||||
t.true(invited, 'failed to share page');
|
||||
|
||||
const revoke = await revokePage(app, u1.token.token, workspace.id, 'page1');
|
||||
t.true(revoke, 'failed to revoke page');
|
||||
const pages2 = await getWorkspaceSharedPages(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id
|
||||
);
|
||||
t.is(pages2.length, 1, 'failed to get shared pages');
|
||||
t.is(pages2[0], 'page2', 'failed to get shared page: page2');
|
||||
|
||||
const msg3 = await revokePage(app, u1.token.token, workspace.id, 'page3');
|
||||
t.false(msg3, 'can revoke non-exists page');
|
||||
|
||||
const msg4 = await revokePage(app, u1.token.token, workspace.id, 'page2');
|
||||
t.true(msg4, 'failed to revoke page');
|
||||
const page3 = await getWorkspaceSharedPages(
|
||||
app,
|
||||
u1.token.token,
|
||||
workspace.id
|
||||
);
|
||||
t.is(page3.length, 0, 'failed to get shared pages');
|
||||
});
|
||||
|
||||
test('should can 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 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');
|
||||
|
||||
t.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');
|
||||
|
||||
t.deepEqual(
|
||||
res2.body,
|
||||
Buffer.from([0, 0]),
|
||||
'failed to get doc with u2 token'
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const isPublic = await updateWorkspace(
|
||||
app,
|
||||
user.token.token,
|
||||
workspace.id,
|
||||
true
|
||||
);
|
||||
|
||||
t.true(isPublic, 'failed to publish workspace');
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
||||
.expect(200)
|
||||
.type('application/octet-stream');
|
||||
|
||||
t.deepEqual(res.body, Buffer.from([0, 0]), 'failed to get public doc');
|
||||
});
|
||||
Reference in New Issue
Block a user