mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
chore(server): data mocking and seeding (#10864)
This commit is contained in:
@@ -40,8 +40,9 @@ yarn affine @affine/server-native build
|
||||
```sh
|
||||
# uncomment all env variables here
|
||||
cp packages/backend/server/.env.example packages/backend/server/.env
|
||||
yarn affine server prisma db push
|
||||
yarn affine server data-migration run
|
||||
|
||||
# everytime there are new migrations, init command should runned again
|
||||
yarn affine server init
|
||||
```
|
||||
|
||||
## Start server
|
||||
@@ -90,3 +91,9 @@ Now you should be able to start developing affine with server enabled.
|
||||
# available at http://localhost:5555
|
||||
yarn affine server prisma studio
|
||||
```
|
||||
|
||||
### Seed the db
|
||||
|
||||
```
|
||||
yarn affine server seed -h
|
||||
```
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"test:coverage": "c8 ava --concurrency 1 --serial",
|
||||
"test:copilot:coverage": "c8 ava --timeout=5m \"src/__tests__/**/copilot-*.spec.ts\"",
|
||||
"data-migration": "cross-env NODE_ENV=development r ./src/data/index.ts",
|
||||
"init": "yarn prisma migrate dev && yarn data-migration run",
|
||||
"seed": "r ./src/seed/index.ts",
|
||||
"predeploy": "yarn prisma migrate deploy && node --import ./scripts/register.js ./dist/data/index.js run",
|
||||
"postinstall": "prisma generate"
|
||||
},
|
||||
@@ -108,6 +110,7 @@
|
||||
"@affine-tools/cli": "workspace:*",
|
||||
"@affine-tools/utils": "workspace:*",
|
||||
"@affine/server-native": "workspace:*",
|
||||
"@faker-js/faker": "^9.6.0",
|
||||
"@nestjs/testing": "^10.4.15",
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@types/express": "^4.17.21",
|
||||
|
||||
@@ -41,7 +41,7 @@ test('change email', async t => {
|
||||
const u1Email = 'u1@affine.pro';
|
||||
const u2Email = 'u2@affine.pro';
|
||||
|
||||
await app.signup(u1Email);
|
||||
await app.signupV1(u1Email);
|
||||
const primitiveMailCount = await getCurrentMailMessageCount();
|
||||
await sendChangeEmail(app, u1Email, 'affine.pro');
|
||||
|
||||
@@ -101,7 +101,7 @@ test('set and change password', async t => {
|
||||
if (mail.hasConfigured()) {
|
||||
const u1Email = 'u1@affine.pro';
|
||||
|
||||
const u1 = await app.signup(u1Email);
|
||||
const u1 = await app.signupV1(u1Email);
|
||||
|
||||
const primitiveMailCount = await getCurrentMailMessageCount();
|
||||
|
||||
@@ -153,7 +153,7 @@ test('should revoke token after change user identify', async t => {
|
||||
const u1Email = 'u1@affine.pro';
|
||||
const u2Email = 'u2@affine.pro';
|
||||
|
||||
const u1 = await app.signup(u1Email);
|
||||
const u1 = await app.signupV1(u1Email);
|
||||
|
||||
{
|
||||
const user = await currentUser(app);
|
||||
@@ -190,7 +190,7 @@ test('should revoke token after change user identify', async t => {
|
||||
const u3Email = 'u3333@affine.pro';
|
||||
|
||||
await app.logout();
|
||||
const u3 = await app.signup(u3Email);
|
||||
const u3 = await app.signupV1(u3Email);
|
||||
|
||||
{
|
||||
const user = await currentUser(app);
|
||||
|
||||
@@ -164,7 +164,7 @@ test('should be able to sign out', async t => {
|
||||
test('should be able to correct user id cookie', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const u1 = await app.signup('u1@affine.pro');
|
||||
const u1 = await app.signupV1('u1@affine.pro');
|
||||
|
||||
const req = app.GET('/api/auth/session');
|
||||
let cookies = req.get('cookie') as unknown as string[];
|
||||
@@ -229,8 +229,8 @@ test('should be able to sign in another account in one session', async t => {
|
||||
test('should be able to sign out multiple accounts in one session', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const u1 = await app.signup('u1@affine.pro');
|
||||
const u2 = await app.signup('u2@affine.pro');
|
||||
const u1 = await app.signupV1('u1@affine.pro');
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
|
||||
// sign out u2
|
||||
await app.GET(`/api/auth/sign-out?user_id=${u2.id}`).expect(200);
|
||||
|
||||
@@ -110,7 +110,7 @@ test.beforeEach(async t => {
|
||||
const { app, prompt } = t.context;
|
||||
await app.initTestingDB();
|
||||
await prompt.onModuleInit();
|
||||
t.context.u1 = await app.signup('u1@affine.pro');
|
||||
t.context.u1 = await app.signupV1('u1@affine.pro');
|
||||
|
||||
unregisterCopilotProvider(OpenAIProvider.type);
|
||||
unregisterCopilotProvider(FalProvider.type);
|
||||
@@ -223,7 +223,7 @@ test('should update session correctly', async t => {
|
||||
}
|
||||
|
||||
{
|
||||
await app.signup('test@affine.pro');
|
||||
await app.signupV1('test@affine.pro');
|
||||
const u2 = await app.createUser('u2@affine.pro');
|
||||
const { id: workspaceId } = await createWorkspace(app);
|
||||
const inviteId = await inviteUser(app, workspaceId, u2.email);
|
||||
@@ -309,7 +309,7 @@ test('should fork session correctly', async t => {
|
||||
}
|
||||
|
||||
{
|
||||
const u2 = await app.signup('u2@affine.pro');
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
await assertForkSession(id, sessionId, randomUUID(), '', async x => {
|
||||
await t.throwsAsync(
|
||||
x,
|
||||
|
||||
@@ -31,8 +31,8 @@ test('should send invite email', async t => {
|
||||
const { mail, app } = t.context;
|
||||
|
||||
if (mail.hasConfigured()) {
|
||||
const u2 = await app.signup('u2@affine.pro');
|
||||
const u1 = await app.signup('u1@affine.pro');
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
const u1 = await app.signupV1('u1@affine.pro');
|
||||
const stub = Sinon.stub(mail, 'send');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
|
||||
90
packages/backend/server/src/__tests__/mocks/factory.ts
Normal file
90
packages/backend/server/src/__tests__/mocks/factory.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Type } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export abstract class Mocker<In, Out> {
|
||||
// NOTE(@forehalo):
|
||||
// The reason why we don't inject [Models] to Mocker for more easier data creation with built in logic is,
|
||||
// the method in [Models] may introduce side effects like 'events',
|
||||
// which may break the tests with event emitting asserts.
|
||||
protected db!: PrismaClient;
|
||||
|
||||
abstract create(input?: Partial<In>): Promise<Out>;
|
||||
}
|
||||
|
||||
type MockerConstructor<In, Out> = Type<Mocker<In, Out>>;
|
||||
type MockerInput<Ctor extends MockerConstructor<any, any>> =
|
||||
Ctor extends MockerConstructor<infer In, any> ? In : never;
|
||||
type MockerOutput<Ctor extends MockerConstructor<any, any>> =
|
||||
Ctor extends MockerConstructor<any, infer Out> ? Out : never;
|
||||
|
||||
const FACTORIES = new Map<string, Mocker<any, any>>();
|
||||
|
||||
interface FactoryOptions {
|
||||
logger: ((val: any) => void) | boolean;
|
||||
}
|
||||
|
||||
export function createFactory(
|
||||
db: PrismaClient,
|
||||
opts: FactoryOptions = { logger: false }
|
||||
) {
|
||||
const log = (val: any) => {
|
||||
if (typeof opts.logger === 'function') {
|
||||
opts.logger(val);
|
||||
} else if (opts.logger) {
|
||||
console.log(val);
|
||||
}
|
||||
};
|
||||
|
||||
class Inner {
|
||||
static create<Ctor extends MockerConstructor<any, any>>(
|
||||
Factory: Ctor,
|
||||
overrides?: Partial<MockerInput<Ctor>>
|
||||
): Promise<MockerOutput<Ctor>>;
|
||||
static create<Ctor extends MockerConstructor<any, any>>(
|
||||
Factory: Ctor,
|
||||
count: number
|
||||
): Promise<MockerOutput<Ctor>[]>;
|
||||
static create<Ctor extends MockerConstructor<any, any>>(
|
||||
Factory: Ctor,
|
||||
overrides: Partial<MockerInput<Ctor>>,
|
||||
count: number
|
||||
): Promise<MockerOutput<Ctor>[]>;
|
||||
static async create<Ctor extends MockerConstructor<any, any>>(
|
||||
Factory: Ctor,
|
||||
overridesOrCount?: Partial<MockerInput<Ctor>> | number,
|
||||
count?: number
|
||||
): Promise<MockerOutput<Ctor> | MockerOutput<Ctor>[]> {
|
||||
let factory = FACTORIES.get(Factory.name);
|
||||
|
||||
if (!factory) {
|
||||
factory = new Factory();
|
||||
// @ts-expect-error private
|
||||
factory.db = db;
|
||||
FACTORIES.set(Factory.name, factory);
|
||||
}
|
||||
|
||||
let overrides: Partial<MockerInput<Ctor>> | undefined = undefined;
|
||||
if (typeof overridesOrCount === 'number') {
|
||||
count = overridesOrCount;
|
||||
} else {
|
||||
overrides = overridesOrCount;
|
||||
}
|
||||
|
||||
if (typeof count === 'number') {
|
||||
return await Promise.all(
|
||||
Array.from({ length: count }).map(async () => {
|
||||
const row = await factory.create(overrides);
|
||||
log(row);
|
||||
return row;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const row = await factory.create(overrides);
|
||||
log(row);
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
return Inner.create;
|
||||
}
|
||||
14
packages/backend/server/src/__tests__/mocks/index.ts
Normal file
14
packages/backend/server/src/__tests__/mocks/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export { createFactory } from './factory';
|
||||
export * from './team-workspace.mock';
|
||||
export * from './user.mock';
|
||||
export * from './workspace.mock';
|
||||
|
||||
import { MockTeamWorkspace } from './team-workspace.mock';
|
||||
import { MockUser } from './user.mock';
|
||||
import { MockWorkspace } from './workspace.mock';
|
||||
|
||||
export const Mockers = {
|
||||
User: MockUser,
|
||||
Workspace: MockWorkspace,
|
||||
TeamWorkspace: MockTeamWorkspace,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { Feature } from '../../models';
|
||||
import { Mocker } from './factory';
|
||||
|
||||
interface MockTeamWorkspaceInput {
|
||||
id: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export class MockTeamWorkspace extends Mocker<
|
||||
MockTeamWorkspaceInput,
|
||||
{ id: string }
|
||||
> {
|
||||
override async create(input?: Partial<MockTeamWorkspaceInput>) {
|
||||
const id = input?.id ?? faker.string.uuid();
|
||||
const quantity = input?.quantity ?? 10;
|
||||
|
||||
await this.db.subscription.create({
|
||||
data: {
|
||||
targetId: id,
|
||||
plan: 'team',
|
||||
recurring: 'monthly',
|
||||
status: 'active',
|
||||
start: faker.date.past(),
|
||||
nextBillAt: faker.date.future(),
|
||||
quantity,
|
||||
},
|
||||
});
|
||||
|
||||
const feature = await this.db.feature.findFirst({
|
||||
where: {
|
||||
name: Feature.TeamPlan,
|
||||
},
|
||||
});
|
||||
|
||||
if (!feature) {
|
||||
throw new Error(
|
||||
`Feature ${Feature.TeamPlan} does not exist in DB. You might forgot to run data-migration first.`
|
||||
);
|
||||
}
|
||||
|
||||
await this.db.workspaceFeature.create({
|
||||
data: {
|
||||
workspaceId: id,
|
||||
featureId: feature.id,
|
||||
reason: 'test',
|
||||
activated: true,
|
||||
configs: {
|
||||
memberLimit: quantity,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { id };
|
||||
}
|
||||
}
|
||||
54
packages/backend/server/src/__tests__/mocks/user.mock.ts
Normal file
54
packages/backend/server/src/__tests__/mocks/user.mock.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { hashSync } from '@node-rs/argon2';
|
||||
import type { Prisma, User } from '@prisma/client';
|
||||
|
||||
import type { UserFeatureName } from '../../models';
|
||||
import { Mocker } from './factory';
|
||||
|
||||
export type MockUserInput = Prisma.UserCreateInput & {
|
||||
feature?: UserFeatureName;
|
||||
};
|
||||
|
||||
export type MockedUser = Omit<User, 'password'> & {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export class MockUser extends Mocker<MockUserInput, MockedUser> {
|
||||
override async create(input?: Partial<MockUserInput>) {
|
||||
const password = input?.password ?? faker.internet.password();
|
||||
const user = await this.db.user.create({
|
||||
data: {
|
||||
email: faker.internet.email(),
|
||||
name: faker.person.fullName(),
|
||||
password: password ? hashSync(password) : undefined,
|
||||
...input,
|
||||
},
|
||||
});
|
||||
|
||||
if (input?.feature) {
|
||||
const feature = await this.db.feature.findFirst({
|
||||
where: {
|
||||
name: input.feature,
|
||||
},
|
||||
});
|
||||
|
||||
if (!feature) {
|
||||
throw new Error(
|
||||
`Feature ${input.feature} does not exist in DB. You might forgot to run data-migration first.`
|
||||
);
|
||||
}
|
||||
|
||||
await this.db.userFeature.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
featureId: feature.id,
|
||||
reason: 'test',
|
||||
activated: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// return raw password for later usage, for example 'signIn'
|
||||
return { ...user, password };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Prisma, Workspace } from '@prisma/client';
|
||||
|
||||
import { WorkspaceRole } from '../../models';
|
||||
import { Mocker } from './factory';
|
||||
|
||||
export type MockWorkspaceInput = Prisma.WorkspaceCreateInput & {
|
||||
owner?: { id: string };
|
||||
};
|
||||
|
||||
export type MockedWorkspace = Workspace;
|
||||
|
||||
export class MockWorkspace extends Mocker<MockWorkspaceInput, MockedWorkspace> {
|
||||
override async create(input?: Partial<MockWorkspaceInput>) {
|
||||
return await this.db.workspace.create({
|
||||
data: {
|
||||
name: faker.animal.cat(),
|
||||
public: false,
|
||||
...input,
|
||||
permissions: input?.owner
|
||||
? {
|
||||
create: {
|
||||
userId: 'id' in input.owner ? input.owner.id : input.owner,
|
||||
type: WorkspaceRole.Owner,
|
||||
status: 'Accepted',
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -191,7 +191,7 @@ test('should use specified throttler for unauthenticated user', async t => {
|
||||
test('should not protect unspecified routes', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
const res = await app.GET('/nonthrottled/default').expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
@@ -204,7 +204,7 @@ test('should not protect unspecified routes', async t => {
|
||||
test('should use default throttler for authenticated user when not specified', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
const res = await app.GET('/throttled/default').expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
@@ -216,7 +216,7 @@ test('should use default throttler for authenticated user when not specified', a
|
||||
test('should use same throttler for multiple routes', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
let res = await app.GET('/throttled/default').expect(200);
|
||||
|
||||
let headers = rateLimitHeaders(res);
|
||||
@@ -235,7 +235,7 @@ test('should use same throttler for multiple routes', async t => {
|
||||
test('should use different throttler if specified', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
let res = await app.GET('/throttled/default').expect(200);
|
||||
|
||||
let headers = rateLimitHeaders(res);
|
||||
@@ -254,7 +254,7 @@ test('should use different throttler if specified', async t => {
|
||||
test('should skip throttler for authenticated if `authenticated` throttler used', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
const res = await app.GET('/throttled/authenticated').expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
@@ -278,7 +278,7 @@ test('should apply `default` throttler for unauthenticated user if `authenticate
|
||||
test('should skip throttler for authenticated user when specified', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
const res = await app.GET('/throttled/skip').expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
@@ -291,7 +291,7 @@ test('should skip throttler for authenticated user when specified', async t => {
|
||||
test('should use specified throttler for authenticated user', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
const res = await app.GET('/throttled/strict').expect(200);
|
||||
|
||||
const headers = rateLimitHeaders(res);
|
||||
@@ -306,7 +306,7 @@ test('should separate anonymous and authenticated user throttlers', async t => {
|
||||
const unauthenticatedUserRes = await app
|
||||
.GET('/nonthrottled/default')
|
||||
.expect(200);
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
const authenticatedUserRes = await app.GET('/throttled/default').expect(200);
|
||||
|
||||
const authenticatedResHeaders = rateLimitHeaders(authenticatedUserRes);
|
||||
|
||||
@@ -82,7 +82,7 @@ const init = async (
|
||||
memberLimit = 10,
|
||||
prefix = randomUUID()
|
||||
) => {
|
||||
const owner = await app.signup(`${prefix}owner@affine.pro`);
|
||||
const owner = await app.signupV1(`${prefix}owner@affine.pro`);
|
||||
const models = app.get(Models);
|
||||
{
|
||||
await models.userFeature.add(owner.id, 'pro_plan_v1', 'test');
|
||||
@@ -101,7 +101,7 @@ const init = async (
|
||||
permission: WorkspaceRole = WorkspaceRole.Collaborator,
|
||||
shouldSendEmail: boolean = false
|
||||
) => {
|
||||
const member = await app.signup(email);
|
||||
const member = await app.signupV1(email);
|
||||
|
||||
{
|
||||
// normal workspace
|
||||
@@ -140,7 +140,7 @@ const init = async (
|
||||
) => {
|
||||
const members = [];
|
||||
for (const email of emails) {
|
||||
const member = await app.signup(email);
|
||||
const member = await app.signupV1(email);
|
||||
members.push(member);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ const init = async (
|
||||
return [
|
||||
inviteId,
|
||||
async (email: string, shouldSendEmail: boolean = false) => {
|
||||
const member = await app.signup(email);
|
||||
const member = await app.signupV1(email);
|
||||
await acceptInviteById(app, ws.id, inviteId, shouldSendEmail);
|
||||
return member;
|
||||
},
|
||||
@@ -221,8 +221,8 @@ test('should be able to invite multiple users', async t => {
|
||||
|
||||
{
|
||||
// manager
|
||||
const m1 = await app.signup('m1@affine.pro');
|
||||
const m2 = await app.signup('m2@affine.pro');
|
||||
const m1 = await app.signupV1('m1@affine.pro');
|
||||
const m2 = await app.signupV1('m2@affine.pro');
|
||||
app.switchUser(owner);
|
||||
t.is(
|
||||
(await inviteUsers(app, ws.id, [m1.email])).length,
|
||||
@@ -483,7 +483,7 @@ test('should be able to approve team member', async t => {
|
||||
const { link } = await createInviteLink(app, tws.id, 'OneDay');
|
||||
const inviteId = link.split('/').pop()!;
|
||||
|
||||
const member = await app.signup('newmember@affine.pro');
|
||||
const member = await app.signupV1('newmember@affine.pro');
|
||||
t.true(
|
||||
await acceptInviteById(app, tws.id, inviteId, false),
|
||||
'should be able to accept invite'
|
||||
|
||||
@@ -25,7 +25,7 @@ test.after.always(async () => {
|
||||
test.skip('should register a user', () => {});
|
||||
|
||||
test('should get current user', async t => {
|
||||
const user = await app.signup('u1@affine.pro');
|
||||
const user = await app.signupV1('u1@affine.pro');
|
||||
const currUser = await currentUser(app);
|
||||
t.is(currUser.id, user.id, 'user.id is not valid');
|
||||
t.is(currUser.name, user.name, 'user.name is not valid');
|
||||
@@ -34,7 +34,7 @@ test('should get current user', async t => {
|
||||
});
|
||||
|
||||
test('should be able to delete user', async t => {
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
const deleted = await deleteAccount(app);
|
||||
t.true(deleted);
|
||||
const currUser = await currentUser(app);
|
||||
|
||||
@@ -19,10 +19,6 @@ test.before(async t => {
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
test.beforeEach(async t => {
|
||||
await t.context.app.initTestingDB();
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
@@ -30,7 +26,7 @@ test.after.always(async t => {
|
||||
test('should be able to upload user avatar', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signup();
|
||||
const avatar = Buffer.from('test');
|
||||
const res = await updateAvatar(app, avatar);
|
||||
|
||||
@@ -46,7 +42,7 @@ test('should be able to upload user avatar', async t => {
|
||||
test('should be able to update user avatar, and invalidate old avatar url', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signup();
|
||||
const avatar = Buffer.from('test');
|
||||
let res = await updateAvatar(app, avatar);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import { INestApplication, ModuleMetadata } from '@nestjs/common';
|
||||
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { TestingModuleBuilder } from '@nestjs/testing';
|
||||
import { User } from '@prisma/client';
|
||||
import { PrismaClient, User } from '@prisma/client';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import supertest from 'supertest';
|
||||
@@ -11,6 +11,7 @@ import supertest from 'supertest';
|
||||
import { AFFiNELogger, ApplyType, GlobalExceptionFilter } from '../../base';
|
||||
import { AuthService } from '../../core/auth';
|
||||
import { UserModel } from '../../models';
|
||||
import { createFactory, MockedUser, MockUser, MockUserInput } from '../mocks';
|
||||
import { createTestingModule } from './testing-module';
|
||||
import { initTestingDB, TEST_LOG_LEVEL } from './utils';
|
||||
|
||||
@@ -80,6 +81,8 @@ export class TestingApp extends ApplyType<INestApplication>() {
|
||||
private currentUserCookie: string | null = null;
|
||||
private readonly userCookies: Set<string> = new Set();
|
||||
|
||||
readonly create!: ReturnType<typeof createFactory>;
|
||||
|
||||
[Symbol.asyncDispose](): Promise<void> {
|
||||
return this.close();
|
||||
}
|
||||
@@ -188,6 +191,9 @@ export class TestingApp extends ApplyType<INestApplication>() {
|
||||
return `test-${randomUUID()}@affine.pro`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `create(MockUser)`
|
||||
*/
|
||||
async createUser(
|
||||
email?: string,
|
||||
override?: Partial<User>
|
||||
@@ -209,13 +215,22 @@ export class TestingApp extends ApplyType<INestApplication>() {
|
||||
return user as Omit<User, 'password'> & { password: string };
|
||||
}
|
||||
|
||||
async signup(email?: string, override?: Partial<User>) {
|
||||
/**
|
||||
* @deprecated use `signup`
|
||||
*/
|
||||
async signupV1(email?: string, override?: Partial<User>) {
|
||||
const user = await this.createUser(email ?? this.randomEmail(), override);
|
||||
await this.login(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
async login(user: TestUser) {
|
||||
async signup(overrides?: Partial<MockUserInput>) {
|
||||
const user = await this.create(MockUser, overrides);
|
||||
await this.login(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
async login(user: MockedUser) {
|
||||
await this.POST('/api/auth/sign-in')
|
||||
.send({
|
||||
email: user.email,
|
||||
@@ -263,6 +278,9 @@ export class TestingApp extends ApplyType<INestApplication>() {
|
||||
function makeTestingApp(app: INestApplication): TestingApp {
|
||||
const testingApp = new TestingApp();
|
||||
|
||||
// @ts-expect-error allow
|
||||
testingApp.create = createFactory(app.get(PrismaClient, { strict: false }));
|
||||
|
||||
return new Proxy(testingApp, {
|
||||
get(target, prop) {
|
||||
// @ts-expect-error override
|
||||
|
||||
@@ -6,12 +6,14 @@ import {
|
||||
TestingModule as BaseTestingModule,
|
||||
TestingModuleBuilder,
|
||||
} from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { AppModule, FunctionalityModules } from '../../app.module';
|
||||
import { AFFiNELogger, Runtime } from '../../base';
|
||||
import { GqlModule } from '../../base/graphql';
|
||||
import { AuthGuard, AuthModule } from '../../core/auth';
|
||||
import { ModelsModule } from '../../models';
|
||||
import { createFactory } from '../mocks';
|
||||
import { initTestingDB, TEST_LOG_LEVEL } from './utils';
|
||||
|
||||
interface TestingModuleMeatdata extends ModuleMetadata {
|
||||
@@ -20,6 +22,7 @@ interface TestingModuleMeatdata extends ModuleMetadata {
|
||||
|
||||
export interface TestingModule extends BaseTestingModule {
|
||||
initTestingDB(): Promise<void>;
|
||||
create: ReturnType<typeof createFactory>;
|
||||
[Symbol.asyncDispose](): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -91,6 +94,10 @@ export async function createTestingModule(
|
||||
await runtime.set('auth/password.min', 1);
|
||||
};
|
||||
|
||||
testingModule.create = createFactory(
|
||||
module.get(PrismaClient, { strict: false })
|
||||
);
|
||||
|
||||
testingModule[Symbol.asyncDispose] = async () => {
|
||||
await module.close();
|
||||
};
|
||||
|
||||
@@ -47,8 +47,8 @@ test.after.always(async t => {
|
||||
|
||||
test('should invite a user', async t => {
|
||||
const { app } = t.context;
|
||||
const u2 = await app.signup('u2@affine.pro');
|
||||
await app.signup('u1@affine.pro');
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
|
||||
@@ -58,8 +58,8 @@ test('should invite a user', async t => {
|
||||
|
||||
test('should leave a workspace', async t => {
|
||||
const { app } = t.context;
|
||||
const u2 = await app.signup('u2@affine.pro');
|
||||
await app.signup('u1@affine.pro');
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
const invite = await inviteUser(app, workspace.id, u2.email);
|
||||
@@ -74,8 +74,8 @@ test('should leave a workspace', async t => {
|
||||
|
||||
test('should revoke a user', async t => {
|
||||
const { app } = t.context;
|
||||
const u2 = await app.signup('u2@affine.pro');
|
||||
await app.signup('u1@affine.pro');
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
await inviteUser(app, workspace.id, u2.email);
|
||||
@@ -89,7 +89,7 @@ test('should revoke a user', async t => {
|
||||
|
||||
test('should create user if not exist', async t => {
|
||||
const { app, models } = t.context;
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
|
||||
@@ -102,8 +102,8 @@ test('should create user if not exist', async t => {
|
||||
|
||||
test('should invite a user by link', async t => {
|
||||
const { app } = t.context;
|
||||
const u2 = await app.signup('u2@affine.pro');
|
||||
const u1 = await app.signup('u1@affine.pro');
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
const u1 = await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
|
||||
@@ -127,8 +127,8 @@ test('should invite a user by link', async t => {
|
||||
test('should send email', async t => {
|
||||
const { mail, app } = t.context;
|
||||
if (mail.hasConfigured()) {
|
||||
const u2 = await app.signup('u2@affine.pro');
|
||||
await app.signup('u1@affine.pro');
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
const primitiveMailCount = await getCurrentMailMessageCount();
|
||||
@@ -193,7 +193,7 @@ test('should send email', async t => {
|
||||
|
||||
test('should support pagination for member', async t => {
|
||||
const { app } = t.context;
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
await inviteUser(app, workspace.id, 'u2@affine.pro');
|
||||
@@ -207,7 +207,7 @@ test('should support pagination for member', async t => {
|
||||
|
||||
test('should limit member count correctly', async t => {
|
||||
const { app } = t.context;
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
await Promise.allSettled(
|
||||
|
||||
@@ -37,7 +37,7 @@ test.after.always(async t => {
|
||||
test('should create a workspace', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
const workspace = await createWorkspace(app);
|
||||
|
||||
t.is(typeof workspace.id, 'string', 'workspace.id is not a string');
|
||||
@@ -45,7 +45,7 @@ test('should create a workspace', async t => {
|
||||
|
||||
test('should be able to publish workspace', async t => {
|
||||
const { app } = t.context;
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
const workspace = await createWorkspace(app);
|
||||
const isPublic = await updateWorkspace(app, workspace.id, true);
|
||||
|
||||
@@ -58,7 +58,7 @@ test('should be able to publish workspace', async t => {
|
||||
|
||||
test('should visit public page', async t => {
|
||||
const { app } = t.context;
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
const share = await publishDoc(app, workspace.id, 'doc1');
|
||||
@@ -104,7 +104,7 @@ test('should visit public page', async t => {
|
||||
test('should not be able to public not permitted doc', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
await app.signup('u2@affine.pro');
|
||||
await app.signupV1('u2@affine.pro');
|
||||
|
||||
await t.throwsAsync(publishDoc(app, 'not_exists_ws', 'doc2'), {
|
||||
message:
|
||||
@@ -119,8 +119,8 @@ test('should not be able to public not permitted doc', async t => {
|
||||
|
||||
test('should be able to get workspace doc', async t => {
|
||||
const { app } = t.context;
|
||||
const u1 = await app.signup('u1@affine.pro');
|
||||
const u2 = await app.signup('u2@affine.pro');
|
||||
const u1 = await app.signupV1('u1@affine.pro');
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
|
||||
app.switchUser(u1.id);
|
||||
const workspace = await createWorkspace(app);
|
||||
@@ -167,7 +167,7 @@ test('should be able to get workspace doc', async t => {
|
||||
|
||||
test('should be able to get public workspace doc', async t => {
|
||||
const { app } = t.context;
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
const isPublic = await updateWorkspace(app, workspace.id, true);
|
||||
|
||||
@@ -40,7 +40,7 @@ test.after.always(async () => {
|
||||
});
|
||||
|
||||
test('should set blobs', async t => {
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
|
||||
@@ -63,7 +63,7 @@ test('should set blobs', async t => {
|
||||
});
|
||||
|
||||
test('should list blobs', async t => {
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
const blobs = await listBlobs(app, workspace.id);
|
||||
@@ -81,7 +81,7 @@ test('should list blobs', async t => {
|
||||
});
|
||||
|
||||
test('should auto delete blobs when workspace is deleted', async t => {
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
const buffer1 = Buffer.from([0, 0]);
|
||||
@@ -100,7 +100,7 @@ test('should auto delete blobs when workspace is deleted', async t => {
|
||||
});
|
||||
|
||||
test('should calc blobs size', async t => {
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
|
||||
@@ -114,7 +114,7 @@ test('should calc blobs size', async t => {
|
||||
});
|
||||
|
||||
test('should calc all blobs size', async t => {
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace1 = await createWorkspace(app);
|
||||
|
||||
@@ -135,7 +135,7 @@ test('should calc all blobs size', async t => {
|
||||
});
|
||||
|
||||
test('should reject blob exceeded limit', async t => {
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace1 = await createWorkspace(app);
|
||||
await model.add(workspace1.id, 'team_plan_v1', 'test', RESTRICTED_QUOTA);
|
||||
@@ -147,7 +147,7 @@ test('should reject blob exceeded limit', async t => {
|
||||
});
|
||||
|
||||
test('should reject blob exceeded quota', async t => {
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
await model.add(workspace.id, 'team_plan_v1', 'test', RESTRICTED_QUOTA);
|
||||
@@ -159,7 +159,7 @@ test('should reject blob exceeded quota', async t => {
|
||||
});
|
||||
|
||||
test('should accept blob even storage out of quota if workspace has unlimited feature', async t => {
|
||||
await app.signup('u1@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
await model.add(workspace.id, 'team_plan_v1', 'test', RESTRICTED_QUOTA);
|
||||
|
||||
@@ -31,7 +31,7 @@ test.before(async t => {
|
||||
|
||||
const db = app.get(PrismaClient);
|
||||
|
||||
t.context.u1 = await app.signup('u1@affine.pro');
|
||||
t.context.u1 = await app.signupV1('u1@affine.pro');
|
||||
t.context.db = db;
|
||||
t.context.app = app;
|
||||
t.context.storage = app.get(WorkspaceBlobStorage);
|
||||
|
||||
@@ -29,8 +29,8 @@ test.after.always(async () => {
|
||||
});
|
||||
|
||||
test('should mention user in a doc', async t => {
|
||||
const member = await app.signup();
|
||||
const owner = await app.signup();
|
||||
const member = await app.signupV1();
|
||||
const owner = await app.signupV1();
|
||||
|
||||
await app.switchUser(owner);
|
||||
const workspace = await createWorkspace(app);
|
||||
@@ -161,8 +161,8 @@ test('should mention doc mode support string value', async t => {
|
||||
});
|
||||
|
||||
test('should throw error when mention user has no Doc.Read role', async t => {
|
||||
const member = await app.signup();
|
||||
const owner = await app.signup();
|
||||
const member = await app.signupV1();
|
||||
const owner = await app.signupV1();
|
||||
|
||||
await app.switchUser(owner);
|
||||
const workspace = await createWorkspace(app);
|
||||
@@ -187,7 +187,7 @@ test('should throw error when mention user has no Doc.Read role', async t => {
|
||||
});
|
||||
|
||||
test('should throw error when mention a not exists user', async t => {
|
||||
const owner = await app.signup();
|
||||
const owner = await app.signupV1();
|
||||
const workspace = await createWorkspace(app);
|
||||
await app.switchUser(owner);
|
||||
const docId = randomUUID();
|
||||
@@ -209,7 +209,7 @@ test('should throw error when mention a not exists user', async t => {
|
||||
});
|
||||
|
||||
test('should not mention user oneself', async t => {
|
||||
const owner = await app.signup();
|
||||
const owner = await app.signupV1();
|
||||
const workspace = await createWorkspace(app);
|
||||
await app.switchUser(owner);
|
||||
await t.throwsAsync(
|
||||
@@ -230,8 +230,8 @@ test('should not mention user oneself', async t => {
|
||||
});
|
||||
|
||||
test('should mark notification as read', async t => {
|
||||
const member = await app.signup();
|
||||
const owner = await app.signup();
|
||||
const member = await app.signupV1();
|
||||
const owner = await app.signupV1();
|
||||
|
||||
await app.switchUser(owner);
|
||||
const workspace = await createWorkspace(app);
|
||||
@@ -273,8 +273,8 @@ test('should mark notification as read', async t => {
|
||||
});
|
||||
|
||||
test('should throw error when read the other user notification', async t => {
|
||||
const member = await app.signup();
|
||||
const owner = await app.signup();
|
||||
const member = await app.signupV1();
|
||||
const owner = await app.signupV1();
|
||||
|
||||
await app.switchUser(owner);
|
||||
const workspace = await createWorkspace(app);
|
||||
@@ -356,8 +356,8 @@ test('should throw error when mention mode value is invalid', async t => {
|
||||
});
|
||||
|
||||
test('should list and count notifications', async t => {
|
||||
const member = await app.signup();
|
||||
const owner = await app.signup();
|
||||
const member = await app.signupV1();
|
||||
const owner = await app.signupV1();
|
||||
|
||||
{
|
||||
await app.switchUser(member);
|
||||
|
||||
88
packages/backend/server/src/seed/index.ts
Normal file
88
packages/backend/server/src/seed/index.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import '../prelude';
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { createFactory, Mockers } from '../__tests__/mocks';
|
||||
|
||||
const client = new PrismaClient();
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (!args.length || args.includes('-h') || args.includes('--help')) {
|
||||
console.log(`
|
||||
seed [Entity] [count] [[field]=[val]]
|
||||
|
||||
Checkout [server/src/__tests__/mocks/*.mock.ts] for all available Entities and Inputs
|
||||
|
||||
examples:
|
||||
|
||||
$ seed User Create an User
|
||||
$ seed User 3 Create 3 Users
|
||||
$ seed User feature=pro_plan_v1 Create an User with Pro feature
|
||||
$ seed TeamWorkspace id=xxx Seed a workspace with Team feature
|
||||
$ seed Workspace id=xxx public=true Seed with boolean property
|
||||
$ seed TeamWorkspace id=xxx quantity=10n Seed with numberic property, use \`={num}n\` suffix
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const name = args.shift() as keyof typeof Mockers;
|
||||
const Mocker = Mockers[name];
|
||||
|
||||
if (!name || !Mocker) {
|
||||
throw new Error(
|
||||
'First argument must be one of: ' + JSON.stringify(Object.keys(Mockers))
|
||||
);
|
||||
}
|
||||
|
||||
const create = createFactory(client, {
|
||||
logger: (val: any) => {
|
||||
console.log(`${name} ${JSON.stringify(val)}`);
|
||||
},
|
||||
});
|
||||
|
||||
function parseArgs(args: string[]) {
|
||||
if (!args.length) {
|
||||
return { count: 1 };
|
||||
}
|
||||
|
||||
const overrides: Record<string, any> = {};
|
||||
let count: number = 1;
|
||||
|
||||
args.forEach(arg => {
|
||||
let kvSep = arg.indexOf('=');
|
||||
if (kvSep) {
|
||||
const key = arg.slice(0, kvSep);
|
||||
const val = arg.slice(kvSep + 1);
|
||||
|
||||
if (/[\d]+n$/.test(val)) {
|
||||
const num = Number(val.slice(0, -1));
|
||||
if (Number.isNaN(num)) {
|
||||
throw new Error(`Invalid numeric parameter: ${arg}`);
|
||||
}
|
||||
overrides[key] = num;
|
||||
} else if (val.length === 4 && val.toLowerCase() === 'true') {
|
||||
overrides[key] = true;
|
||||
} else if (val.length === 5 && val.toLowerCase() === 'false') {
|
||||
overrides[key] = false;
|
||||
} else {
|
||||
overrides[key] = val;
|
||||
}
|
||||
} else {
|
||||
const maybeCount = parseInt(arg);
|
||||
if (!maybeCount || Number.isNaN(maybeCount)) {
|
||||
console.warn(`Invalid parameter: ${arg}`);
|
||||
return;
|
||||
}
|
||||
count = maybeCount;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
overrides,
|
||||
count,
|
||||
};
|
||||
}
|
||||
|
||||
const { overrides, count } = parseArgs(args);
|
||||
await create(Mocker, overrides as any, count);
|
||||
@@ -834,6 +834,7 @@ __metadata:
|
||||
"@affine/server-native": "workspace:*"
|
||||
"@apollo/server": "npm:^4.11.2"
|
||||
"@aws-sdk/client-s3": "npm:^3.709.0"
|
||||
"@faker-js/faker": "npm:^9.6.0"
|
||||
"@fal-ai/serverless-client": "npm:^0.15.0"
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "npm:^0.20.0"
|
||||
"@google-cloud/opentelemetry-cloud-trace-exporter": "npm:^2.4.1"
|
||||
@@ -5105,7 +5106,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@faker-js/faker@npm:^9.3.0":
|
||||
"@faker-js/faker@npm:^9.3.0, @faker-js/faker@npm:^9.6.0":
|
||||
version: 9.6.0
|
||||
resolution: "@faker-js/faker@npm:9.6.0"
|
||||
checksum: 10/f53aeca972a16e7cbb26024c457cea7e1c6bff9dd60561f942a48eb3233863ed7b5fbb1392eb98a0901cb5bea23cf7dbb0793a30c1655478c6a76a43ebb6c360
|
||||
|
||||
Reference in New Issue
Block a user