diff --git a/packages/backend/server/src/app.ts b/packages/backend/server/src/app.ts index b123026318..ea47c41a32 100644 --- a/packages/backend/server/src/app.ts +++ b/packages/backend/server/src/app.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { DynamicModule, Module, Type } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { AppController } from './app.controller'; @@ -13,7 +13,7 @@ import { PrismaModule } from './prisma'; import { SessionModule } from './session'; import { RateLimiterModule } from './throttler'; -export const BasicModules = [ +export const FunctionalityModules: Array = [ ConfigModule.forRoot(), CacheModule, PrismaModule, @@ -26,7 +26,7 @@ export const BasicModules = [ // better module registration logic if (AFFiNE.redis.enabled) { - BasicModules.push(RedisModule); + FunctionalityModules.push(RedisModule); } @Module({ @@ -36,7 +36,7 @@ if (AFFiNE.redis.enabled) { useClass: CacheInterceptor, }, ], - imports: [...BasicModules, ...BusinessModules], + imports: [...FunctionalityModules, ...BusinessModules], controllers: SERVER_FLAVOR === 'selfhosted' ? [] : [AppController], }) export class AppModule {} diff --git a/packages/backend/server/src/graphql.module.ts b/packages/backend/server/src/graphql.module.ts index 38ded5c1df..71610e2235 100644 --- a/packages/backend/server/src/graphql.module.ts +++ b/packages/backend/server/src/graphql.module.ts @@ -21,11 +21,12 @@ import { GQLLoggerPlugin } from './graphql/logger-plugin'; csrfPrevention: { requestHeaders: ['content-type'], }, - autoSchemaFile: join( - fileURLToPath(import.meta.url), - '..', - 'schema.gql' - ), + autoSchemaFile: config.node.test + ? join( + fileURLToPath(import.meta.url), + '../../node_modules/.cache/schema.gql' + ) + : join(fileURLToPath(import.meta.url), '..', 'schema.gql'), context: ({ req, res }: { req: Request; res: Response }) => ({ req, res, diff --git a/packages/backend/server/src/modules/quota/index.ts b/packages/backend/server/src/modules/quota/index.ts index 2fc8c8edd5..224422fa17 100644 --- a/packages/backend/server/src/modules/quota/index.ts +++ b/packages/backend/server/src/modules/quota/index.ts @@ -12,6 +12,7 @@ import { QuotaManagementService } from './storage'; * - quota statistics */ @Module({ + // FIXME: Quota really need to know `Storage`? imports: [StorageModule], providers: [PermissionService, QuotaService, QuotaManagementService], exports: [QuotaService, QuotaManagementService], diff --git a/packages/backend/server/src/modules/storage/__tests__/fs.spec.ts b/packages/backend/server/src/modules/storage/__tests__/fs.spec.ts index 242c7d5664..3f1f36b874 100644 --- a/packages/backend/server/src/modules/storage/__tests__/fs.spec.ts +++ b/packages/backend/server/src/modules/storage/__tests__/fs.spec.ts @@ -101,7 +101,7 @@ test('list recursively', async t => { t.is(r5.length, 20); }); -test.only('delete', async t => { +test('delete', async t => { const provider = createProvider(); const key = 'testKey'; const body = Buffer.from('testBody'); diff --git a/packages/backend/server/src/prisma/index.ts b/packages/backend/server/src/prisma/index.ts index b3f3b330d8..1c5c8e257b 100644 --- a/packages/backend/server/src/prisma/index.ts +++ b/packages/backend/server/src/prisma/index.ts @@ -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'; +// both `PrismaService` and `PrismaClient` can be injected +const clientProvider: Provider = { + provide: PrismaClient, + useExisting: PrismaService, +}; + @Global() @Module({ - providers: [PrismaService], - exports: [PrismaService], + providers: [PrismaService, clientProvider], + exports: [PrismaService, clientProvider], }) export class PrismaModule {} export { PrismaService } from './service'; diff --git a/packages/backend/server/tests/app.e2e.ts b/packages/backend/server/tests/app.e2e.ts index 175f58b597..600737252e 100644 --- a/packages/backend/server/tests/app.e2e.ts +++ b/packages/backend/server/tests/app.e2e.ts @@ -3,17 +3,16 @@ 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 { type User } from '@prisma/client'; import ava, { type TestFn } from 'ava'; import type { Express } from 'express'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; import { AppModule } from '../src/app'; import { FeatureManagementService } from '../src/modules/features'; import { PrismaService } from '../src/prisma/service'; +import { createTestingApp } from './utils'; const gql = '/graphql'; @@ -49,25 +48,18 @@ class FakePrisma { } test.beforeEach(async t => { - const module = await Test.createTestingModule({ + const { app } = await createTestingApp({ imports: [AppModule], - }) - .overrideProvider(PrismaService) - .useClass(FakePrisma) - .overrideProvider(FeatureManagementService) - .useValue({ canEarlyAccess: () => true }) - .compile(); - t.context.app = module.createNestApplication({ - cors: true, - bodyParser: true, + tapModule(builder) { + builder + .overrideProvider(PrismaService) + .useClass(FakePrisma) + .overrideProvider(FeatureManagementService) + .useValue({ canEarlyAccess: () => true }); + }, }); - t.context.app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await t.context.app.init(); + + t.context.app = app; }); test.afterEach.always(async t => { diff --git a/packages/backend/server/tests/auth.e2e.ts b/packages/backend/server/tests/auth.e2e.ts index a159a8ae6b..440c189ca6 100644 --- a/packages/backend/server/tests/auth.e2e.ts +++ b/packages/backend/server/tests/auth.e2e.ts @@ -3,19 +3,13 @@ import { 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, { 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 { AuthService } from '../src/modules/auth/service'; import { changeEmail, - createWorkspace, - initFeatureConfigs, + createTestingApp, sendChangeEmail, sendVerifyChangeEmail, signUp, @@ -23,44 +17,20 @@ import { 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], - 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); + const { app } = await createTestingApp(); + const auth = app.get(AuthService); + const mail = app.get(MailService); t.context.app = app; t.context.auth = auth; t.context.mail = mail; - - // init features - await initFeatureConfigs(module); }); -test.afterEach(async t => { +test.afterEach.always(async t => { await t.context.app.close(); }); @@ -73,8 +43,6 @@ test('change email', async t => { 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'); diff --git a/packages/backend/server/tests/auth.spec.ts b/packages/backend/server/tests/auth.spec.ts index e87125b796..906182ea2a 100644 --- a/packages/backend/server/tests/auth.spec.ts +++ b/packages/backend/server/tests/auth.spec.ts @@ -1,34 +1,19 @@ /// -import { Test, TestingModule } from '@nestjs/testing'; -import { PrismaClient } from '@prisma/client'; +import { TestingModule } from '@nestjs/testing'; import test from 'ava'; -import { CacheModule } from '../src/cache'; 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 { AuthService } from '../src/modules/auth/service'; -import { PrismaModule } from '../src/prisma'; import { mintChallengeResponse, verifyChallengeResponse } from '../src/storage'; -import { RateLimiterModule } from '../src/throttler'; -import { initFeatureConfigs } from './utils'; +import { createTestingModule } from './utils'; 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({ + module = await createTestingModule({ imports: [ ConfigModule.forRoot({ auth: { @@ -39,20 +24,11 @@ test.beforeEach(async () => { host: 'example.org', https: true, }), - PrismaModule, - CacheModule, - GqlModule, - AuthModule, - RateLimiterModule, - RevertCommand, - RunCommand, ], - }).compile(); + }); + authService = module.get(AuthService); authResolver = module.get(AuthResolver); - - // init features - await initFeatureConfigs(module); }); test.afterEach.always(async () => { diff --git a/packages/backend/server/tests/doc.spec.ts b/packages/backend/server/tests/doc.spec.ts index e004d697ec..d772514c24 100644 --- a/packages/backend/server/tests/doc.spec.ts +++ b/packages/backend/server/tests/doc.spec.ts @@ -1,9 +1,7 @@ import { mock } from 'node:test'; -import type { INestApplication } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing'; import test from 'ava'; -import { register } from 'prom-client'; import * as Sinon from 'sinon'; import { applyUpdate, @@ -12,37 +10,19 @@ import { encodeStateAsUpdate, } from 'yjs'; -import { CacheModule } from '../src/cache'; -import { Config, ConfigModule } from '../src/config'; -import { - collectMigrations, - RevertCommand, - RunCommand, -} from '../src/data/commands/run'; -import { EventModule } from '../src/event'; +import { Config } from '../src/config'; import { DocManager, DocModule } from '../src/modules/doc'; import { QuotaModule } from '../src/modules/quota'; import { StorageModule } from '../src/modules/storage'; -import { PrismaModule, PrismaService } from '../src/prisma'; -import { flushDB } from './utils'; +import { PrismaService } from '../src/prisma'; +import { createTestingModule, initTestingDB } from './utils'; const createModule = () => { - return Test.createTestingModule({ - imports: [ - PrismaModule, - CacheModule, - EventModule, - QuotaModule, - StorageModule, - ConfigModule.forRoot(), - DocModule, - RevertCommand, - RunCommand, - ], - }).compile(); + return createTestingModule({ + imports: [QuotaModule, StorageModule, DocModule], + }); }; -let app: INestApplication; let m: TestingModule; let timer: Sinon.SinonFakeTimers; @@ -51,44 +31,34 @@ test.beforeEach(async () => { timer = Sinon.useFakeTimers({ toFake: ['setInterval'], }); - await flushDB(); m = await createModule(); - app = m.createNestApplication(); - app.enableShutdownHooks(); - 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(); + await m.init(); + await initTestingDB(m.get(PrismaService)); }); 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(); + await m.init(); t.is(fake.mock.callCount(), 1); // @ts-expect-error private member t.truthy(manager.job); + m.close(); }); test('should be able to stop poll', async t => { const manager = m.get(DocManager); const fake = mock.method(manager, 'destroy'); - await app.close(); + await m.close(); t.is(fake.mock.callCount(), 1); // @ts-expect-error private member diff --git a/packages/backend/server/tests/feature.spec.ts b/packages/backend/server/tests/feature.spec.ts index 320f970898..20012f7806 100644 --- a/packages/backend/server/tests/feature.spec.ts +++ b/packages/backend/server/tests/feature.spec.ts @@ -1,14 +1,9 @@ /// -import { Injectable } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { PrismaClient } from '@prisma/client'; +import { INestApplication, Injectable } from '@nestjs/common'; 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 { AuthModule } from '../src/modules/auth'; import { AuthService } from '../src/modules/auth/service'; import { FeatureManagementService, @@ -19,9 +14,8 @@ import { import { UserType } from '../src/modules/users/types'; import { WorkspaceResolver } from '../src/modules/workspaces/resolvers'; import { Permission } from '../src/modules/workspaces/types'; -import { PrismaModule, PrismaService } from '../src/prisma'; -import { RateLimiterModule } from '../src/throttler'; -import { initFeatureConfigs } from './utils'; +import { PrismaService } from '../src/prisma'; +import { createTestingApp } from './utils'; @Injectable() class WorkspaceResolverMock { @@ -53,20 +47,11 @@ const test = ava as TestFn<{ feature: FeatureService; workspace: WorkspaceResolver; 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 => { - const module = await Test.createTestingModule({ + const { app } = await createTestingApp({ imports: [ ConfigModule.forRoot({ auth: { @@ -80,28 +65,21 @@ test.beforeEach(async t => { earlyAccessPreview: true, }, }), - PrismaModule, - CacheModule, - AuthModule, FeatureModule, - RateLimiterModule, - RevertCommand, - RunCommand, ], providers: [WorkspaceResolver], - }) - .overrideProvider(WorkspaceResolver) - .useClass(WorkspaceResolverMock) - .compile(); + tapModule: module => { + module + .overrideProvider(WorkspaceResolver) + .useClass(WorkspaceResolverMock); + }, + }); - t.context.app = module; - t.context.auth = module.get(AuthService); - t.context.feature = module.get(FeatureService); - t.context.workspace = module.get(WorkspaceResolver); - t.context.management = module.get(FeatureManagementService); - - // init features - await initFeatureConfigs(module); + t.context.app = app; + t.context.auth = app.get(AuthService); + t.context.feature = app.get(FeatureService); + t.context.workspace = app.get(WorkspaceResolver); + t.context.management = app.get(FeatureManagementService); }); test.afterEach.always(async t => { diff --git a/packages/backend/server/tests/history.spec.ts b/packages/backend/server/tests/history.spec.ts index 050f30de56..7d416bd08d 100644 --- a/packages/backend/server/tests/history.spec.ts +++ b/packages/backend/server/tests/history.spec.ts @@ -1,40 +1,26 @@ -import { INestApplication } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; -import { Test, TestingModule } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing'; import type { Snapshot } from '@prisma/client'; import test from 'ava'; import * as Sinon from 'sinon'; -import { ConfigModule } from '../src/config'; -import { EventModule, type EventPayload } from '../src/event'; +import { type EventPayload } from '../src/event'; import { DocHistoryManager } from '../src/modules/doc'; import { QuotaModule } from '../src/modules/quota'; import { StorageModule } from '../src/modules/storage'; -import { PrismaModule, PrismaService } from '../src/prisma'; -import { flushDB } from './utils'; +import { PrismaService } from '../src/prisma'; +import { createTestingModule } from './utils'; -let app: INestApplication; let m: TestingModule; let manager: DocHistoryManager; let db: PrismaService; // cleanup database before each test test.beforeEach(async () => { - await flushDB(); - m = await Test.createTestingModule({ - imports: [ - PrismaModule, - QuotaModule, - EventModule, - StorageModule, - ScheduleModule.forRoot(), - ConfigModule.forRoot(), - ], + m = await createTestingModule({ + imports: [StorageModule, QuotaModule], providers: [DocHistoryManager], - }).compile(); + }); - app = m.createNestApplication(); - await app.init(); manager = m.get(DocHistoryManager); Sinon.stub(manager, 'getExpiredDateFromNow').resolves( new Date(Date.now() + 1000) @@ -42,8 +28,7 @@ test.beforeEach(async () => { db = m.get(PrismaService); }); -test.afterEach(async () => { - await app.close(); +test.afterEach.always(async () => { await m.close(); Sinon.restore(); }); diff --git a/packages/backend/server/tests/mailer.e2e.ts b/packages/backend/server/tests/mailer.e2e.ts index 3538d4e303..0e948dfcc9 100644 --- a/packages/backend/server/tests/mailer.e2e.ts +++ b/packages/backend/server/tests/mailer.e2e.ts @@ -6,19 +6,13 @@ import { getCurrentMailMessageCount, getLatestMailMessage, } from '@affine-test/kit/utils/cloud'; -import { Test, TestingModule } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; 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 { GqlModule } from '../src/graphql.module'; -import { AuthModule } from '../src/modules/auth'; import { AuthService } from '../src/modules/auth/service'; -import { PrismaModule } from '../src/prisma'; -import { RateLimiterModule } from '../src/throttler'; -import { initFeatureConfigs } from './utils'; +import { createTestingModule } from './utils'; const test = ava as TestFn<{ auth: AuthService; @@ -34,7 +28,7 @@ test.beforeEach(async () => { }); test.beforeEach(async t => { - t.context.module = await Test.createTestingModule({ + t.context.module = await createTestingModule({ imports: [ ConfigModule.forRoot({ auth: { @@ -43,18 +37,9 @@ test.beforeEach(async t => { leeway: 1, }, }), - PrismaModule, - GqlModule, - AuthModule, - CacheModule, - RateLimiterModule, ], - providers: [RevertCommand, RunCommand], - }).compile(); + }); t.context.auth = t.context.module.get(AuthService); - - // init features - await initFeatureConfigs(t.context.module); }); test.afterEach.always(async t => { diff --git a/packages/backend/server/tests/mailer.spec.ts b/packages/backend/server/tests/mailer.spec.ts index 1e1da0c2ef..7f45664ec6 100644 --- a/packages/backend/server/tests/mailer.spec.ts +++ b/packages/backend/server/tests/mailer.spec.ts @@ -1,18 +1,22 @@ import { randomUUID } from 'node:crypto'; import type { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; import { hashSync } from '@node-rs/argon2'; import { type User } from '@prisma/client'; import ava, { type TestFn } from 'ava'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import { AppModule } from '../src/app'; import { MailService } from '../src/modules/auth/mailer'; import { FeatureKind, FeatureManagementService } from '../src/modules/features'; import { Quotas } from '../src/modules/quota'; import { PrismaService } from '../src/prisma'; -import { createWorkspace, getInviteInfo, inviteUser, signUp } from './utils'; +import { + createTestingApp, + createWorkspace, + getInviteInfo, + inviteUser, + signUp, +} from './utils'; const FakePrisma = { fakeUser: { @@ -123,26 +127,20 @@ const test = ava as TestFn<{ }>; test.beforeEach(async t => { - const module = await Test.createTestingModule({ + const { module, app } = await createTestingApp({ imports: [AppModule], - }) - .overrideProvider(PrismaService) - .useValue(FakePrisma) - .overrideProvider(FeatureManagementService) - .useValue({ - hasWorkspaceFeature() { - return false; - }, - }) - .compile(); - const app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); + tapModule: module => { + module + .overrideProvider(PrismaService) + .useValue(FakePrisma) + .overrideProvider(FeatureManagementService) + .useValue({ + hasWorkspaceFeature() { + return false; + }, + }); + }, + }); const mail = module.get(MailService); t.context.app = app; diff --git a/packages/backend/server/tests/quota.spec.ts b/packages/backend/server/tests/quota.spec.ts index 5e5237759a..75c7662a04 100644 --- a/packages/backend/server/tests/quota.spec.ts +++ b/packages/backend/server/tests/quota.spec.ts @@ -1,14 +1,8 @@ /// -import { Test, TestingModule } from '@nestjs/testing'; -import { PrismaClient } from '@prisma/client'; +import { TestingModule } from '@nestjs/testing'; 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 { QuotaManagementService, @@ -18,64 +12,32 @@ import { QuotaType, } from '../src/modules/quota'; import { StorageModule } from '../src/modules/storage'; -import { PrismaModule } from '../src/prisma'; -import { RateLimiterModule } from '../src/throttler'; -import { initFeatureConfigs } from './utils'; +import { createTestingModule } from './utils'; const test = ava as TestFn<{ auth: AuthService; quota: QuotaService; 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 => { - const module = await Test.createTestingModule({ - imports: [ - 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 module = await createTestingModule({ + imports: [StorageModule, QuotaModule], + }); const quota = module.get(QuotaService); const storageQuota = module.get(QuotaManagementService); const auth = module.get(AuthService); - t.context.app = module; + t.context.module = module; t.context.quota = quota; t.context.storageQuota = storageQuota; t.context.auth = auth; - - // init features - await initFeatureConfigs(module); }); test.afterEach.always(async t => { - await t.context.app.close(); + await t.context.module.close(); }); test('should be able to set quota', async t => { diff --git a/packages/backend/server/tests/session.spec.ts b/packages/backend/server/tests/session.spec.ts index 71b08a485a..8814011d39 100644 --- a/packages/backend/server/tests/session.spec.ts +++ b/packages/backend/server/tests/session.spec.ts @@ -1,19 +1,20 @@ /// -import { Test, TestingModule } from '@nestjs/testing'; +import { TestingModule } from '@nestjs/testing'; import ava, { type TestFn } from 'ava'; import { CacheModule } from '../src/cache'; import { ConfigModule } from '../src/config'; import { SessionModule, SessionService } from '../src/session'; +import { createTestingModule } from './utils'; const test = ava as TestFn<{ session: SessionService; - app: TestingModule; + module: TestingModule; }>; test.beforeEach(async t => { - const module = await Test.createTestingModule({ + const module = await createTestingModule({ imports: [ ConfigModule.forRoot({ redis: { @@ -23,14 +24,14 @@ test.beforeEach(async t => { CacheModule, SessionModule, ], - }).compile(); + }); const session = module.get(SessionService); - t.context.app = module; + t.context.module = module; t.context.session = session; }); test.afterEach.always(async t => { - await t.context.app.close(); + await t.context.module.close(); }); test('should be able to set session', async t => { diff --git a/packages/backend/server/tests/user.e2e.ts b/packages/backend/server/tests/user.e2e.ts index 7087ba3f9f..3932282a44 100644 --- a/packages/backend/server/tests/user.e2e.ts +++ b/packages/backend/server/tests/user.e2e.ts @@ -1,40 +1,17 @@ import type { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import { PrismaClient } from '@prisma/client'; import test from 'ava'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; import { AppModule } from '../src/app'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; -import { currentUser, initFeatureConfigs, signUp } from './utils'; +import { createTestingApp, 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({ + const { app: testApp } = await createTestingApp({ imports: [AppModule], - providers: [RevertCommand, RunCommand], - }).compile(); - app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - await app.init(); - - // init features - await initFeatureConfigs(module); + }); + app = testApp; }); test.afterEach.always(async () => { diff --git a/packages/backend/server/tests/utils/utils.ts b/packages/backend/server/tests/utils/utils.ts index 042f718c93..334a78941e 100644 --- a/packages/backend/server/tests/utils/utils.ts +++ b/packages/backend/server/tests/utils/utils.ts @@ -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 { hashSync } from '@node-rs/argon2'; -import { PrismaClient, type User } from '@prisma/client'; +import { AppModule, FunctionalityModules } from '../../src/app'; +import { UserFeaturesInit1698652531198 } from '../../src/data/migrations/1698652531198-user-features-init'; +import { GqlModule } from '../../src/graphql.module'; -import { RevertCommand, RunCommand } from '../../src/data/commands/run'; - -export async function flushDB() { - const client = new PrismaClient(); - await client.$connect(); +async function flushDB(client: PrismaClient) { const result: { tablename: string }[] = await client.$queryRaw`SELECT tablename FROM pg_catalog.pg_tables @@ -22,41 +22,99 @@ export async function flushDB() { .filter(name => !name.includes('migrations')) .join(', ')}` ); - - await client.$disconnect(); } -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(), - }; +async function initFeatureConfigs(db: PrismaClient) { + await UserFeaturesInit1698652531198.up(db); +} - 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 async function initTestingDB(db: PrismaClient) { + await flushDB(db); + await initFeatureConfigs(db); +} + +interface TestingModuleMeatdata extends ModuleMetadata { + tapModule?(m: TestingModuleBuilder): void; + tapApp?(app: INestApplication): void; +} + +function dedupeModules(modules: NonNullable) { + const map = new Map(); + + modules.forEach(m => { + if ('module' in m) { + map.set(m.module, m); + } else { + map.set(m, m); + } + }); + + return Array.from(map.values()); +} + +@Resolver(() => String) +class MockResolver { + @Query(() => String) + hello() { + return 'hello world'; } } -export async function initFeatureConfigs(module: TestingModule) { - const run = module.get(RunCommand); - const revert = module.get(RevertCommand); - await Promise.allSettled([revert.run(['UserFeaturesInit1698652531198'])]); - await run.runOne('UserFeaturesInit1698652531198'); +export async function createTestingModule( + moduleDef: TestingModuleMeatdata = {} +) { + // setting up + 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, + }; } diff --git a/packages/backend/server/tests/workspace-blobs.spec.ts b/packages/backend/server/tests/workspace-blobs.spec.ts index ed86723e8a..37abda1e45 100644 --- a/packages/backend/server/tests/workspace-blobs.spec.ts +++ b/packages/backend/server/tests/workspace-blobs.spec.ts @@ -1,19 +1,15 @@ import type { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import { PrismaClient } from '@prisma/client'; import test from 'ava'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; import { AppModule } from '../src/app'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; import { QuotaService, QuotaType } from '../src/modules/quota'; import { checkBlobSize, collectAllBlobSizes, + createTestingApp, createWorkspace, getWorkspaceBlobsSize, - initFeatureConfigs, listBlobs, setBlob, signUp, @@ -22,36 +18,13 @@ import { let app: INestApplication; let quota: QuotaService; -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({ + const { app: testApp } = await createTestingApp({ imports: [AppModule], - providers: [RevertCommand, RunCommand], - }).compile(); - app = module.createNestApplication(); - app.use( - graphqlUploadExpress({ - maxFileSize: 10 * 1024 * 1024, - maxFiles: 5, - }) - ); - quota = module.get(QuotaService); + }); - // init features - await initFeatureConfigs(module); - - await app.init(); + app = testApp; + quota = app.get(QuotaService); }); test.afterEach.always(async () => { diff --git a/packages/backend/server/tests/workspace-invite.e2e.ts b/packages/backend/server/tests/workspace-invite.e2e.ts index 52a6eac694..8ecc747290 100644 --- a/packages/backend/server/tests/workspace-invite.e2e.ts +++ b/packages/backend/server/tests/workspace-invite.e2e.ts @@ -3,20 +3,18 @@ import { 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, { 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 { AuthService } from '../src/modules/auth/service'; +import { PrismaService } from '../src/prisma'; import { acceptInviteById, + createTestingApp, createWorkspace, getWorkspace, - initFeatureConfigs, inviteUser, leaveWorkspace, revokeUser, @@ -31,36 +29,13 @@ const test = ava as TestFn<{ }>; 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({ + const { app } = await createTestingApp({ 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.auth = auth; - t.context.mail = mail; - - // init features - await initFeatureConfigs(module); + t.context.client = app.get(PrismaService); + t.context.auth = app.get(AuthService); + t.context.mail = app.get(MailService); }); test.afterEach.always(async t => { diff --git a/packages/backend/server/tests/workspace.e2e.ts b/packages/backend/server/tests/workspace.e2e.ts index 86904acc6a..8483a92141 100644 --- a/packages/backend/server/tests/workspace.e2e.ts +++ b/packages/backend/server/tests/workspace.e2e.ts @@ -1,19 +1,17 @@ import type { INestApplication } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import ava, { type TestFn } from 'ava'; -import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; import { AppModule } from '../src/app'; -import { RevertCommand, RunCommand } from '../src/data/commands/run'; +import { PrismaService } from '../src/prisma'; import { acceptInviteById, + createTestingApp, createWorkspace, currentUser, getPublicWorkspace, getWorkspacePublicPages, - initFeatureConfigs, inviteUser, publishPage, revokePublicPage, @@ -27,30 +25,12 @@ const test = ava as TestFn<{ }>; 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({ + const { app } = await createTestingApp({ 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 - await initFeatureConfigs(module); + t.context.client = app.get(PrismaService); + t.context.app = app; }); test.afterEach.always(async t => {