test(server): make server testing utils (#5544)

This commit is contained in:
liuyi
2024-01-11 06:40:55 +00:00
parent 9253e522aa
commit 12fdb18a80
20 changed files with 238 additions and 451 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common'; import { DynamicModule, Module, Type } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core'; import { APP_INTERCEPTOR } from '@nestjs/core';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
@@ -13,7 +13,7 @@ import { PrismaModule } from './prisma';
import { SessionModule } from './session'; import { SessionModule } from './session';
import { RateLimiterModule } from './throttler'; import { RateLimiterModule } from './throttler';
export const BasicModules = [ export const FunctionalityModules: Array<Type | DynamicModule> = [
ConfigModule.forRoot(), ConfigModule.forRoot(),
CacheModule, CacheModule,
PrismaModule, PrismaModule,
@@ -26,7 +26,7 @@ export const BasicModules = [
// better module registration logic // better module registration logic
if (AFFiNE.redis.enabled) { if (AFFiNE.redis.enabled) {
BasicModules.push(RedisModule); FunctionalityModules.push(RedisModule);
} }
@Module({ @Module({
@@ -36,7 +36,7 @@ if (AFFiNE.redis.enabled) {
useClass: CacheInterceptor, useClass: CacheInterceptor,
}, },
], ],
imports: [...BasicModules, ...BusinessModules], imports: [...FunctionalityModules, ...BusinessModules],
controllers: SERVER_FLAVOR === 'selfhosted' ? [] : [AppController], controllers: SERVER_FLAVOR === 'selfhosted' ? [] : [AppController],
}) })
export class AppModule {} export class AppModule {}
@@ -21,11 +21,12 @@ import { GQLLoggerPlugin } from './graphql/logger-plugin';
csrfPrevention: { csrfPrevention: {
requestHeaders: ['content-type'], requestHeaders: ['content-type'],
}, },
autoSchemaFile: join( autoSchemaFile: config.node.test
fileURLToPath(import.meta.url), ? join(
'..', fileURLToPath(import.meta.url),
'schema.gql' '../../node_modules/.cache/schema.gql'
), )
: join(fileURLToPath(import.meta.url), '..', 'schema.gql'),
context: ({ req, res }: { req: Request; res: Response }) => ({ context: ({ req, res }: { req: Request; res: Response }) => ({
req, req,
res, res,
@@ -12,6 +12,7 @@ import { QuotaManagementService } from './storage';
* - quota statistics * - quota statistics
*/ */
@Module({ @Module({
// FIXME: Quota really need to know `Storage`?
imports: [StorageModule], imports: [StorageModule],
providers: [PermissionService, QuotaService, QuotaManagementService], providers: [PermissionService, QuotaService, QuotaManagementService],
exports: [QuotaService, QuotaManagementService], exports: [QuotaService, QuotaManagementService],
@@ -101,7 +101,7 @@ test('list recursively', async t => {
t.is(r5.length, 20); t.is(r5.length, 20);
}); });
test.only('delete', async t => { test('delete', async t => {
const provider = createProvider(); const provider = createProvider();
const key = 'testKey'; const key = 'testKey';
const body = Buffer.from('testBody'); const body = Buffer.from('testBody');
+10 -3
View File
@@ -1,11 +1,18 @@
import { Global, Module } from '@nestjs/common'; import { Global, Module, Provider } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { PrismaService } from './service'; import { PrismaService } from './service';
// both `PrismaService` and `PrismaClient` can be injected
const clientProvider: Provider = {
provide: PrismaClient,
useExisting: PrismaService,
};
@Global() @Global()
@Module({ @Module({
providers: [PrismaService], providers: [PrismaService, clientProvider],
exports: [PrismaService], exports: [PrismaService, clientProvider],
}) })
export class PrismaModule {} export class PrismaModule {}
export { PrismaService } from './service'; export { PrismaService } from './service';
+11 -19
View File
@@ -3,17 +3,16 @@ import { randomUUID } from 'node:crypto';
import { Transformer } from '@napi-rs/image'; import { Transformer } from '@napi-rs/image';
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { hashSync } from '@node-rs/argon2'; import { hashSync } from '@node-rs/argon2';
import { type User } from '@prisma/client'; import { type User } from '@prisma/client';
import ava, { type TestFn } from 'ava'; import ava, { type TestFn } from 'ava';
import type { Express } from 'express'; import type { Express } from 'express';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import request from 'supertest'; import request from 'supertest';
import { AppModule } from '../src/app'; import { AppModule } from '../src/app';
import { FeatureManagementService } from '../src/modules/features'; import { FeatureManagementService } from '../src/modules/features';
import { PrismaService } from '../src/prisma/service'; import { PrismaService } from '../src/prisma/service';
import { createTestingApp } from './utils';
const gql = '/graphql'; const gql = '/graphql';
@@ -49,25 +48,18 @@ class FakePrisma {
} }
test.beforeEach(async t => { test.beforeEach(async t => {
const module = await Test.createTestingModule({ const { app } = await createTestingApp({
imports: [AppModule], imports: [AppModule],
}) tapModule(builder) {
.overrideProvider(PrismaService) builder
.useClass(FakePrisma) .overrideProvider(PrismaService)
.overrideProvider(FeatureManagementService) .useClass(FakePrisma)
.useValue({ canEarlyAccess: () => true }) .overrideProvider(FeatureManagementService)
.compile(); .useValue({ canEarlyAccess: () => true });
t.context.app = module.createNestApplication({ },
cors: true,
bodyParser: true,
}); });
t.context.app.use(
graphqlUploadExpress({ t.context.app = app;
maxFileSize: 10 * 1024 * 1024,
maxFiles: 5,
})
);
await t.context.app.init();
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {
+5 -37
View File
@@ -3,19 +3,13 @@ import {
getLatestMailMessage, getLatestMailMessage,
} from '@affine-test/kit/utils/cloud'; } from '@affine-test/kit/utils/cloud';
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import ava, { type TestFn } from 'ava'; import ava, { type TestFn } from 'ava';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { AppModule } from '../src/app';
import { RevertCommand, RunCommand } from '../src/data/commands/run';
import { MailService } from '../src/modules/auth/mailer'; import { MailService } from '../src/modules/auth/mailer';
import { AuthService } from '../src/modules/auth/service'; import { AuthService } from '../src/modules/auth/service';
import { import {
changeEmail, changeEmail,
createWorkspace, createTestingApp,
initFeatureConfigs,
sendChangeEmail, sendChangeEmail,
sendVerifyChangeEmail, sendVerifyChangeEmail,
signUp, signUp,
@@ -23,44 +17,20 @@ import {
const test = ava as TestFn<{ const test = ava as TestFn<{
app: INestApplication; app: INestApplication;
client: PrismaClient;
auth: AuthService; auth: AuthService;
mail: MailService; mail: MailService;
}>; }>;
test.beforeEach(async t => { test.beforeEach(async t => {
const client = new PrismaClient(); const { app } = await createTestingApp();
t.context.client = client; const auth = app.get(AuthService);
await client.$connect(); const mail = app.get(MailService);
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],
providers: [RevertCommand, RunCommand],
}).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.app = app;
t.context.auth = auth; t.context.auth = auth;
t.context.mail = mail; t.context.mail = mail;
// init features
await initFeatureConfigs(module);
}); });
test.afterEach(async t => { test.afterEach.always(async t => {
await t.context.app.close(); await t.context.app.close();
}); });
@@ -73,8 +43,6 @@ test('change email', async t => {
const u1 = await signUp(app, 'u1', u1Email, '1'); const u1 = await signUp(app, 'u1', u1Email, '1');
await createWorkspace(app, u1.token.token);
const primitiveMailCount = await getCurrentMailMessageCount(); const primitiveMailCount = await getCurrentMailMessageCount();
await sendChangeEmail(app, u1.token.token, u1Email, 'affine.pro'); await sendChangeEmail(app, u1.token.token, u1Email, 'affine.pro');
+5 -29
View File
@@ -1,34 +1,19 @@
/// <reference types="../src/global.d.ts" /> /// <reference types="../src/global.d.ts" />
import { Test, TestingModule } from '@nestjs/testing'; import { TestingModule } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import test from 'ava'; import test from 'ava';
import { CacheModule } from '../src/cache';
import { ConfigModule } from '../src/config'; import { ConfigModule } from '../src/config';
import { RevertCommand, RunCommand } from '../src/data/commands/run';
import { GqlModule } from '../src/graphql.module';
import { AuthModule } from '../src/modules/auth';
import { AuthResolver } from '../src/modules/auth/resolver'; import { AuthResolver } from '../src/modules/auth/resolver';
import { AuthService } from '../src/modules/auth/service'; import { AuthService } from '../src/modules/auth/service';
import { PrismaModule } from '../src/prisma';
import { mintChallengeResponse, verifyChallengeResponse } from '../src/storage'; import { mintChallengeResponse, verifyChallengeResponse } from '../src/storage';
import { RateLimiterModule } from '../src/throttler'; import { createTestingModule } from './utils';
import { initFeatureConfigs } from './utils';
let authService: AuthService; let authService: AuthService;
let authResolver: AuthResolver; let authResolver: AuthResolver;
let module: TestingModule; let module: TestingModule;
// cleanup database before each test
test.beforeEach(async () => { test.beforeEach(async () => {
const client = new PrismaClient(); module = await createTestingModule({
await client.$connect();
await client.user.deleteMany({});
await client.$disconnect();
});
test.beforeEach(async () => {
module = await Test.createTestingModule({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
auth: { auth: {
@@ -39,20 +24,11 @@ test.beforeEach(async () => {
host: 'example.org', host: 'example.org',
https: true, https: true,
}), }),
PrismaModule,
CacheModule,
GqlModule,
AuthModule,
RateLimiterModule,
RevertCommand,
RunCommand,
], ],
}).compile(); });
authService = module.get(AuthService); authService = module.get(AuthService);
authResolver = module.get(AuthResolver); authResolver = module.get(AuthResolver);
// init features
await initFeatureConfigs(module);
}); });
test.afterEach.always(async () => { test.afterEach.always(async () => {
+12 -42
View File
@@ -1,9 +1,7 @@
import { mock } from 'node:test'; import { mock } from 'node:test';
import type { INestApplication } from '@nestjs/common'; import { TestingModule } from '@nestjs/testing';
import { Test, TestingModule } from '@nestjs/testing';
import test from 'ava'; import test from 'ava';
import { register } from 'prom-client';
import * as Sinon from 'sinon'; import * as Sinon from 'sinon';
import { import {
applyUpdate, applyUpdate,
@@ -12,37 +10,19 @@ import {
encodeStateAsUpdate, encodeStateAsUpdate,
} from 'yjs'; } from 'yjs';
import { CacheModule } from '../src/cache'; import { Config } from '../src/config';
import { Config, ConfigModule } from '../src/config';
import {
collectMigrations,
RevertCommand,
RunCommand,
} from '../src/data/commands/run';
import { EventModule } from '../src/event';
import { DocManager, DocModule } from '../src/modules/doc'; import { DocManager, DocModule } from '../src/modules/doc';
import { QuotaModule } from '../src/modules/quota'; import { QuotaModule } from '../src/modules/quota';
import { StorageModule } from '../src/modules/storage'; import { StorageModule } from '../src/modules/storage';
import { PrismaModule, PrismaService } from '../src/prisma'; import { PrismaService } from '../src/prisma';
import { flushDB } from './utils'; import { createTestingModule, initTestingDB } from './utils';
const createModule = () => { const createModule = () => {
return Test.createTestingModule({ return createTestingModule({
imports: [ imports: [QuotaModule, StorageModule, DocModule],
PrismaModule, });
CacheModule,
EventModule,
QuotaModule,
StorageModule,
ConfigModule.forRoot(),
DocModule,
RevertCommand,
RunCommand,
],
}).compile();
}; };
let app: INestApplication;
let m: TestingModule; let m: TestingModule;
let timer: Sinon.SinonFakeTimers; let timer: Sinon.SinonFakeTimers;
@@ -51,44 +31,34 @@ test.beforeEach(async () => {
timer = Sinon.useFakeTimers({ timer = Sinon.useFakeTimers({
toFake: ['setInterval'], toFake: ['setInterval'],
}); });
await flushDB();
m = await createModule(); m = await createModule();
app = m.createNestApplication(); await m.init();
app.enableShutdownHooks(); await initTestingDB(m.get(PrismaService));
await app.init();
// init features
const run = m.get(RunCommand);
const revert = m.get(RevertCommand);
const migrations = await collectMigrations();
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
await run.run();
}); });
test.afterEach.always(async () => { test.afterEach.always(async () => {
await app.close();
await m.close(); await m.close();
timer.restore(); timer.restore();
}); });
test('should setup update poll interval', async t => { test('should setup update poll interval', async t => {
register.clear();
const m = await createModule(); const m = await createModule();
const manager = m.get(DocManager); const manager = m.get(DocManager);
const fake = mock.method(manager, 'setup'); const fake = mock.method(manager, 'setup');
await m.createNestApplication().init(); await m.init();
t.is(fake.mock.callCount(), 1); t.is(fake.mock.callCount(), 1);
// @ts-expect-error private member // @ts-expect-error private member
t.truthy(manager.job); t.truthy(manager.job);
m.close();
}); });
test('should be able to stop poll', async t => { test('should be able to stop poll', async t => {
const manager = m.get(DocManager); const manager = m.get(DocManager);
const fake = mock.method(manager, 'destroy'); const fake = mock.method(manager, 'destroy');
await app.close(); await m.close();
t.is(fake.mock.callCount(), 1); t.is(fake.mock.callCount(), 1);
// @ts-expect-error private member // @ts-expect-error private member
+16 -38
View File
@@ -1,14 +1,9 @@
/// <reference types="../src/global.d.ts" /> /// <reference types="../src/global.d.ts" />
import { Injectable } from '@nestjs/common'; import { INestApplication, Injectable } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import ava, { type TestFn } from 'ava'; import ava, { type TestFn } from 'ava';
import { CacheModule } from '../src/cache';
import { ConfigModule } from '../src/config'; import { ConfigModule } from '../src/config';
import { RevertCommand, RunCommand } from '../src/data/commands/run';
import { AuthModule } from '../src/modules/auth';
import { AuthService } from '../src/modules/auth/service'; import { AuthService } from '../src/modules/auth/service';
import { import {
FeatureManagementService, FeatureManagementService,
@@ -19,9 +14,8 @@ import {
import { UserType } from '../src/modules/users/types'; import { UserType } from '../src/modules/users/types';
import { WorkspaceResolver } from '../src/modules/workspaces/resolvers'; import { WorkspaceResolver } from '../src/modules/workspaces/resolvers';
import { Permission } from '../src/modules/workspaces/types'; import { Permission } from '../src/modules/workspaces/types';
import { PrismaModule, PrismaService } from '../src/prisma'; import { PrismaService } from '../src/prisma';
import { RateLimiterModule } from '../src/throttler'; import { createTestingApp } from './utils';
import { initFeatureConfigs } from './utils';
@Injectable() @Injectable()
class WorkspaceResolverMock { class WorkspaceResolverMock {
@@ -53,20 +47,11 @@ const test = ava as TestFn<{
feature: FeatureService; feature: FeatureService;
workspace: WorkspaceResolver; workspace: WorkspaceResolver;
management: FeatureManagementService; management: FeatureManagementService;
app: TestingModule; app: INestApplication;
}>; }>;
// cleanup database before each test
test.beforeEach(async () => {
const client = new PrismaClient();
await client.$connect();
await client.user.deleteMany({});
await client.workspace.deleteMany({});
await client.$disconnect();
});
test.beforeEach(async t => { test.beforeEach(async t => {
const module = await Test.createTestingModule({ const { app } = await createTestingApp({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
auth: { auth: {
@@ -80,28 +65,21 @@ test.beforeEach(async t => {
earlyAccessPreview: true, earlyAccessPreview: true,
}, },
}), }),
PrismaModule,
CacheModule,
AuthModule,
FeatureModule, FeatureModule,
RateLimiterModule,
RevertCommand,
RunCommand,
], ],
providers: [WorkspaceResolver], providers: [WorkspaceResolver],
}) tapModule: module => {
.overrideProvider(WorkspaceResolver) module
.useClass(WorkspaceResolverMock) .overrideProvider(WorkspaceResolver)
.compile(); .useClass(WorkspaceResolverMock);
},
});
t.context.app = module; t.context.app = app;
t.context.auth = module.get(AuthService); t.context.auth = app.get(AuthService);
t.context.feature = module.get(FeatureService); t.context.feature = app.get(FeatureService);
t.context.workspace = module.get(WorkspaceResolver); t.context.workspace = app.get(WorkspaceResolver);
t.context.management = module.get(FeatureManagementService); t.context.management = app.get(FeatureManagementService);
// init features
await initFeatureConfigs(module);
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {
+8 -23
View File
@@ -1,40 +1,26 @@
import { INestApplication } from '@nestjs/common'; import { TestingModule } from '@nestjs/testing';
import { ScheduleModule } from '@nestjs/schedule';
import { Test, TestingModule } from '@nestjs/testing';
import type { Snapshot } from '@prisma/client'; import type { Snapshot } from '@prisma/client';
import test from 'ava'; import test from 'ava';
import * as Sinon from 'sinon'; import * as Sinon from 'sinon';
import { ConfigModule } from '../src/config'; import { type EventPayload } from '../src/event';
import { EventModule, type EventPayload } from '../src/event';
import { DocHistoryManager } from '../src/modules/doc'; import { DocHistoryManager } from '../src/modules/doc';
import { QuotaModule } from '../src/modules/quota'; import { QuotaModule } from '../src/modules/quota';
import { StorageModule } from '../src/modules/storage'; import { StorageModule } from '../src/modules/storage';
import { PrismaModule, PrismaService } from '../src/prisma'; import { PrismaService } from '../src/prisma';
import { flushDB } from './utils'; import { createTestingModule } from './utils';
let app: INestApplication;
let m: TestingModule; let m: TestingModule;
let manager: DocHistoryManager; let manager: DocHistoryManager;
let db: PrismaService; let db: PrismaService;
// cleanup database before each test // cleanup database before each test
test.beforeEach(async () => { test.beforeEach(async () => {
await flushDB(); m = await createTestingModule({
m = await Test.createTestingModule({ imports: [StorageModule, QuotaModule],
imports: [
PrismaModule,
QuotaModule,
EventModule,
StorageModule,
ScheduleModule.forRoot(),
ConfigModule.forRoot(),
],
providers: [DocHistoryManager], providers: [DocHistoryManager],
}).compile(); });
app = m.createNestApplication();
await app.init();
manager = m.get(DocHistoryManager); manager = m.get(DocHistoryManager);
Sinon.stub(manager, 'getExpiredDateFromNow').resolves( Sinon.stub(manager, 'getExpiredDateFromNow').resolves(
new Date(Date.now() + 1000) new Date(Date.now() + 1000)
@@ -42,8 +28,7 @@ test.beforeEach(async () => {
db = m.get(PrismaService); db = m.get(PrismaService);
}); });
test.afterEach(async () => { test.afterEach.always(async () => {
await app.close();
await m.close(); await m.close();
Sinon.restore(); Sinon.restore();
}); });
+4 -19
View File
@@ -6,19 +6,13 @@ import {
getCurrentMailMessageCount, getCurrentMailMessageCount,
getLatestMailMessage, getLatestMailMessage,
} from '@affine-test/kit/utils/cloud'; } from '@affine-test/kit/utils/cloud';
import { Test, TestingModule } from '@nestjs/testing'; import { TestingModule } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import ava, { type TestFn } from 'ava'; import ava, { type TestFn } from 'ava';
import { CacheModule } from '../src/cache';
import { ConfigModule } from '../src/config'; import { ConfigModule } from '../src/config';
import { RevertCommand, RunCommand } from '../src/data/commands/run';
import { GqlModule } from '../src/graphql.module';
import { AuthModule } from '../src/modules/auth';
import { AuthService } from '../src/modules/auth/service'; import { AuthService } from '../src/modules/auth/service';
import { PrismaModule } from '../src/prisma'; import { createTestingModule } from './utils';
import { RateLimiterModule } from '../src/throttler';
import { initFeatureConfigs } from './utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
auth: AuthService; auth: AuthService;
@@ -34,7 +28,7 @@ test.beforeEach(async () => {
}); });
test.beforeEach(async t => { test.beforeEach(async t => {
t.context.module = await Test.createTestingModule({ t.context.module = await createTestingModule({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
auth: { auth: {
@@ -43,18 +37,9 @@ test.beforeEach(async t => {
leeway: 1, leeway: 1,
}, },
}), }),
PrismaModule,
GqlModule,
AuthModule,
CacheModule,
RateLimiterModule,
], ],
providers: [RevertCommand, RunCommand], });
}).compile();
t.context.auth = t.context.module.get(AuthService); t.context.auth = t.context.module.get(AuthService);
// init features
await initFeatureConfigs(t.context.module);
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {
+20 -22
View File
@@ -1,18 +1,22 @@
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { hashSync } from '@node-rs/argon2'; import { hashSync } from '@node-rs/argon2';
import { type User } from '@prisma/client'; import { type User } from '@prisma/client';
import ava, { type TestFn } from 'ava'; import ava, { type TestFn } from 'ava';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { AppModule } from '../src/app'; import { AppModule } from '../src/app';
import { MailService } from '../src/modules/auth/mailer'; import { MailService } from '../src/modules/auth/mailer';
import { FeatureKind, FeatureManagementService } from '../src/modules/features'; import { FeatureKind, FeatureManagementService } from '../src/modules/features';
import { Quotas } from '../src/modules/quota'; import { Quotas } from '../src/modules/quota';
import { PrismaService } from '../src/prisma'; import { PrismaService } from '../src/prisma';
import { createWorkspace, getInviteInfo, inviteUser, signUp } from './utils'; import {
createTestingApp,
createWorkspace,
getInviteInfo,
inviteUser,
signUp,
} from './utils';
const FakePrisma = { const FakePrisma = {
fakeUser: { fakeUser: {
@@ -123,26 +127,20 @@ const test = ava as TestFn<{
}>; }>;
test.beforeEach(async t => { test.beforeEach(async t => {
const module = await Test.createTestingModule({ const { module, app } = await createTestingApp({
imports: [AppModule], imports: [AppModule],
}) tapModule: module => {
.overrideProvider(PrismaService) module
.useValue(FakePrisma) .overrideProvider(PrismaService)
.overrideProvider(FeatureManagementService) .useValue(FakePrisma)
.useValue({ .overrideProvider(FeatureManagementService)
hasWorkspaceFeature() { .useValue({
return false; hasWorkspaceFeature() {
}, return false;
}) },
.compile(); });
const app = module.createNestApplication(); },
app.use( });
graphqlUploadExpress({
maxFileSize: 10 * 1024 * 1024,
maxFiles: 5,
})
);
await app.init();
const mail = module.get(MailService); const mail = module.get(MailService);
t.context.app = app; t.context.app = app;
+8 -46
View File
@@ -1,14 +1,8 @@
/// <reference types="../src/global.d.ts" /> /// <reference types="../src/global.d.ts" />
import { Test, TestingModule } from '@nestjs/testing'; import { TestingModule } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import ava, { type TestFn } from 'ava'; import ava, { type TestFn } from 'ava';
import { CacheModule } from '../src/cache';
import { ConfigModule } from '../src/config';
import { RevertCommand, RunCommand } from '../src/data/commands/run';
import { EventModule } from '../src/event';
import { AuthModule } from '../src/modules/auth';
import { AuthService } from '../src/modules/auth/service'; import { AuthService } from '../src/modules/auth/service';
import { import {
QuotaManagementService, QuotaManagementService,
@@ -18,64 +12,32 @@ import {
QuotaType, QuotaType,
} from '../src/modules/quota'; } from '../src/modules/quota';
import { StorageModule } from '../src/modules/storage'; import { StorageModule } from '../src/modules/storage';
import { PrismaModule } from '../src/prisma'; import { createTestingModule } from './utils';
import { RateLimiterModule } from '../src/throttler';
import { initFeatureConfigs } from './utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
auth: AuthService; auth: AuthService;
quota: QuotaService; quota: QuotaService;
storageQuota: QuotaManagementService; storageQuota: QuotaManagementService;
app: TestingModule; 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 t => { test.beforeEach(async t => {
const module = await Test.createTestingModule({ const module = await createTestingModule({
imports: [ imports: [StorageModule, QuotaModule],
ConfigModule.forRoot({ });
auth: {
accessTokenExpiresIn: 1,
refreshTokenExpiresIn: 1,
leeway: 1,
},
host: 'example.org',
https: true,
}),
PrismaModule,
CacheModule,
AuthModule,
EventModule,
QuotaModule,
StorageModule,
RateLimiterModule,
RevertCommand,
RunCommand,
],
}).compile();
const quota = module.get(QuotaService); const quota = module.get(QuotaService);
const storageQuota = module.get(QuotaManagementService); const storageQuota = module.get(QuotaManagementService);
const auth = module.get(AuthService); const auth = module.get(AuthService);
t.context.app = module; t.context.module = module;
t.context.quota = quota; t.context.quota = quota;
t.context.storageQuota = storageQuota; t.context.storageQuota = storageQuota;
t.context.auth = auth; t.context.auth = auth;
// init features
await initFeatureConfigs(module);
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {
await t.context.app.close(); await t.context.module.close();
}); });
test('should be able to set quota', async t => { test('should be able to set quota', async t => {
@@ -1,19 +1,20 @@
/// <reference types="../src/global.d.ts" /> /// <reference types="../src/global.d.ts" />
import { Test, TestingModule } from '@nestjs/testing'; import { TestingModule } from '@nestjs/testing';
import ava, { type TestFn } from 'ava'; import ava, { type TestFn } from 'ava';
import { CacheModule } from '../src/cache'; import { CacheModule } from '../src/cache';
import { ConfigModule } from '../src/config'; import { ConfigModule } from '../src/config';
import { SessionModule, SessionService } from '../src/session'; import { SessionModule, SessionService } from '../src/session';
import { createTestingModule } from './utils';
const test = ava as TestFn<{ const test = ava as TestFn<{
session: SessionService; session: SessionService;
app: TestingModule; module: TestingModule;
}>; }>;
test.beforeEach(async t => { test.beforeEach(async t => {
const module = await Test.createTestingModule({ const module = await createTestingModule({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
redis: { redis: {
@@ -23,14 +24,14 @@ test.beforeEach(async t => {
CacheModule, CacheModule,
SessionModule, SessionModule,
], ],
}).compile(); });
const session = module.get(SessionService); const session = module.get(SessionService);
t.context.app = module; t.context.module = module;
t.context.session = session; t.context.session = session;
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {
await t.context.app.close(); await t.context.module.close();
}); });
test('should be able to set session', async t => { test('should be able to set session', async t => {
+4 -27
View File
@@ -1,40 +1,17 @@
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import test from 'ava'; import test from 'ava';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import request from 'supertest'; import request from 'supertest';
import { AppModule } from '../src/app'; import { AppModule } from '../src/app';
import { RevertCommand, RunCommand } from '../src/data/commands/run'; import { createTestingApp, currentUser, signUp } from './utils';
import { currentUser, initFeatureConfigs, signUp } from './utils';
let app: INestApplication; let app: INestApplication;
// cleanup database before each test
test.beforeEach(async () => { test.beforeEach(async () => {
const client = new PrismaClient(); const { app: testApp } = await createTestingApp({
await client.$connect();
await client.user.deleteMany({});
await client.$disconnect();
});
test.beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [AppModule], imports: [AppModule],
providers: [RevertCommand, RunCommand], });
}).compile(); app = testApp;
app = module.createNestApplication();
app.use(
graphqlUploadExpress({
maxFileSize: 10 * 1024 * 1024,
maxFiles: 5,
})
);
await app.init();
// init features
await initFeatureConfigs(module);
}); });
test.afterEach.always(async () => { test.afterEach.always(async () => {
+98 -40
View File
@@ -1,14 +1,14 @@
import { randomUUID } from 'node:crypto'; import { INestApplication, ModuleMetadata } from '@nestjs/common';
import { Query, Resolver } from '@nestjs/graphql';
import { Test, TestingModuleBuilder } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { TestingModule } from '@nestjs/testing'; import { AppModule, FunctionalityModules } from '../../src/app';
import { hashSync } from '@node-rs/argon2'; import { UserFeaturesInit1698652531198 } from '../../src/data/migrations/1698652531198-user-features-init';
import { PrismaClient, type User } from '@prisma/client'; import { GqlModule } from '../../src/graphql.module';
import { RevertCommand, RunCommand } from '../../src/data/commands/run'; async function flushDB(client: PrismaClient) {
export async function flushDB() {
const client = new PrismaClient();
await client.$connect();
const result: { tablename: string }[] = const result: { tablename: string }[] =
await client.$queryRaw`SELECT tablename await client.$queryRaw`SELECT tablename
FROM pg_catalog.pg_tables FROM pg_catalog.pg_tables
@@ -22,41 +22,99 @@ export async function flushDB() {
.filter(name => !name.includes('migrations')) .filter(name => !name.includes('migrations'))
.join(', ')}` .join(', ')}`
); );
await client.$disconnect();
} }
export class FakePrisma { async function initFeatureConfigs(db: PrismaClient) {
fakeUser: User = { await UserFeaturesInit1698652531198.up(db);
id: randomUUID(), }
name: 'Alex Yang',
avatarUrl: '',
email: 'alex.yang@example.org',
password: hashSync('123456'),
emailVerified: new Date(),
createdAt: new Date(),
};
get user() { export async function initTestingDB(db: PrismaClient) {
// eslint-disable-next-line @typescript-eslint/no-this-alias await flushDB(db);
const prisma = this; await initFeatureConfigs(db);
return { }
async findFirst() {
return prisma.fakeUser; interface TestingModuleMeatdata extends ModuleMetadata {
}, tapModule?(m: TestingModuleBuilder): void;
async findUnique() { tapApp?(app: INestApplication): void;
return this.findFirst(); }
},
async update() { function dedupeModules(modules: NonNullable<ModuleMetadata['imports']>) {
return this.findFirst(); const map = new Map();
},
}; modules.forEach(m => {
if ('module' in m) {
map.set(m.module, m);
} else {
map.set(m, m);
}
});
return Array.from(map.values());
}
@Resolver(() => String)
class MockResolver {
@Query(() => String)
hello() {
return 'hello world';
} }
} }
export async function initFeatureConfigs(module: TestingModule) { export async function createTestingModule(
const run = module.get(RunCommand); moduleDef: TestingModuleMeatdata = {}
const revert = module.get(RevertCommand); ) {
await Promise.allSettled([revert.run(['UserFeaturesInit1698652531198'])]); // setting up
await run.runOne('UserFeaturesInit1698652531198'); let imports = moduleDef.imports ?? [];
imports =
imports[0] === AppModule
? [AppModule]
: dedupeModules([...FunctionalityModules, GqlModule, ...imports]);
const builder = Test.createTestingModule({
imports,
providers: [MockResolver, ...(moduleDef.providers ?? [])],
controllers: moduleDef.controllers,
});
if (moduleDef.tapModule) {
moduleDef.tapModule(builder);
}
const m = await builder.compile();
const prisma = m.get(PrismaClient);
if (prisma instanceof PrismaClient) {
await flushDB(prisma);
await initFeatureConfigs(prisma);
}
return m;
}
export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
const m = await createTestingModule(moduleDef);
const app = m.createNestApplication({
cors: true,
bodyParser: true,
rawBody: true,
});
app.use(
graphqlUploadExpress({
maxFileSize: 10 * 1024 * 1024,
maxFiles: 5,
})
);
if (moduleDef.tapApp) {
moduleDef.tapApp(app);
}
await app.init();
return {
module: m,
app,
};
} }
@@ -1,19 +1,15 @@
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import test from 'ava'; import test from 'ava';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import request from 'supertest'; import request from 'supertest';
import { AppModule } from '../src/app'; import { AppModule } from '../src/app';
import { RevertCommand, RunCommand } from '../src/data/commands/run';
import { QuotaService, QuotaType } from '../src/modules/quota'; import { QuotaService, QuotaType } from '../src/modules/quota';
import { import {
checkBlobSize, checkBlobSize,
collectAllBlobSizes, collectAllBlobSizes,
createTestingApp,
createWorkspace, createWorkspace,
getWorkspaceBlobsSize, getWorkspaceBlobsSize,
initFeatureConfigs,
listBlobs, listBlobs,
setBlob, setBlob,
signUp, signUp,
@@ -22,36 +18,13 @@ import {
let app: INestApplication; let app: INestApplication;
let quota: QuotaService; let quota: QuotaService;
const client = new PrismaClient();
// cleanup database before each test
test.beforeEach(async () => { test.beforeEach(async () => {
await client.$connect(); const { app: testApp } = await createTestingApp({
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], imports: [AppModule],
providers: [RevertCommand, RunCommand], });
}).compile();
app = module.createNestApplication();
app.use(
graphqlUploadExpress({
maxFileSize: 10 * 1024 * 1024,
maxFiles: 5,
})
);
quota = module.get(QuotaService);
// init features app = testApp;
await initFeatureConfigs(module); quota = app.get(QuotaService);
await app.init();
}); });
test.afterEach.always(async () => { test.afterEach.always(async () => {
@@ -3,20 +3,18 @@ import {
getLatestMailMessage, getLatestMailMessage,
} from '@affine-test/kit/utils/cloud'; } from '@affine-test/kit/utils/cloud';
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import ava, { type TestFn } from 'ava'; import ava, { type TestFn } from 'ava';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { AppModule } from '../src/app'; import { AppModule } from '../src/app';
import { RevertCommand, RunCommand } from '../src/data/commands/run';
import { MailService } from '../src/modules/auth/mailer'; import { MailService } from '../src/modules/auth/mailer';
import { AuthService } from '../src/modules/auth/service'; import { AuthService } from '../src/modules/auth/service';
import { PrismaService } from '../src/prisma';
import { import {
acceptInviteById, acceptInviteById,
createTestingApp,
createWorkspace, createWorkspace,
getWorkspace, getWorkspace,
initFeatureConfigs,
inviteUser, inviteUser,
leaveWorkspace, leaveWorkspace,
revokeUser, revokeUser,
@@ -31,36 +29,13 @@ const test = ava as TestFn<{
}>; }>;
test.beforeEach(async t => { test.beforeEach(async t => {
const client = new PrismaClient(); const { app } = await createTestingApp({
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], imports: [AppModule],
providers: [RevertCommand, RunCommand], });
}).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.app = app;
t.context.auth = auth; t.context.client = app.get(PrismaService);
t.context.mail = mail; t.context.auth = app.get(AuthService);
t.context.mail = app.get(MailService);
// init features
await initFeatureConfigs(module);
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {
+6 -26
View File
@@ -1,19 +1,17 @@
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import ava, { type TestFn } from 'ava'; import ava, { type TestFn } from 'ava';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import request from 'supertest'; import request from 'supertest';
import { AppModule } from '../src/app'; import { AppModule } from '../src/app';
import { RevertCommand, RunCommand } from '../src/data/commands/run'; import { PrismaService } from '../src/prisma';
import { import {
acceptInviteById, acceptInviteById,
createTestingApp,
createWorkspace, createWorkspace,
currentUser, currentUser,
getPublicWorkspace, getPublicWorkspace,
getWorkspacePublicPages, getWorkspacePublicPages,
initFeatureConfigs,
inviteUser, inviteUser,
publishPage, publishPage,
revokePublicPage, revokePublicPage,
@@ -27,30 +25,12 @@ const test = ava as TestFn<{
}>; }>;
test.beforeEach(async t => { test.beforeEach(async t => {
const client = new PrismaClient(); const { app } = await createTestingApp({
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], imports: [AppModule],
providers: [RevertCommand, RunCommand], });
}).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;
// init features t.context.client = app.get(PrismaService);
await initFeatureConfigs(module); t.context.app = app;
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {