From 9b5d12dc7142f8f8c8c07be6f0312774a0aa5029 Mon Sep 17 00:00:00 2001 From: forehalo Date: Mon, 17 Mar 2025 10:02:12 +0000 Subject: [PATCH] test(server): new test facilities (#10870) close CLOUD-142 --- .../charts/graphql/templates/deployment.yaml | 2 - .github/workflows/build-test.yml | 74 +++++++++++++++-- ...estjs-testing-npm-10.4.15-d591a1705a.patch | 36 +++++++++ packages/backend/server/ava.config.js | 31 ++++++++ packages/backend/server/package.json | 39 +-------- .../server/src/__tests__/create-module.ts | 49 ++++++++++++ .../server/src/__tests__/e2e/app.spec.ts | 5 ++ .../server/src/__tests__/e2e/create-app.ts | 79 +++++++++++++++++++ .../server/src/__tests__/e2e/prelude.ts | 4 + .../backend/server/src/__tests__/e2e/test.ts | 9 +++ .../src/__tests__/utils/testing-module.ts | 6 ++ yarn.lock | 23 +++++- 12 files changed, 311 insertions(+), 46 deletions(-) create mode 100644 .yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch create mode 100644 packages/backend/server/ava.config.js create mode 100644 packages/backend/server/src/__tests__/create-module.ts create mode 100644 packages/backend/server/src/__tests__/e2e/app.spec.ts create mode 100644 packages/backend/server/src/__tests__/e2e/create-app.ts create mode 100644 packages/backend/server/src/__tests__/e2e/prelude.ts create mode 100644 packages/backend/server/src/__tests__/e2e/test.ts diff --git a/.github/helm/affine/charts/graphql/templates/deployment.yaml b/.github/helm/affine/charts/graphql/templates/deployment.yaml index a2f5b004c3..bf6e06b9ac 100644 --- a/.github/helm/affine/charts/graphql/templates/deployment.yaml +++ b/.github/helm/affine/charts/graphql/templates/deployment.yaml @@ -77,8 +77,6 @@ spec: value: "{{ .Values.app.https }}" - name: ENABLE_R2_OBJECT_STORAGE value: "{{ .Values.global.objectStorage.r2.enabled }}" - - name: FEATURES_EARLY_ACCESS_PREVIEW - value: "{{ .Values.app.features.earlyAccessPreview }}" - name: FEATURES_SYNC_CLIENT_VERSION_CHECK value: "{{ .Values.app.features.syncClientVersionCheck }}" - name: MAILER_HOST diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 2eceb891b1..53821ec4fd 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -452,7 +452,6 @@ jobs: total_nodes: [3] env: NODE_ENV: test - DISTRIBUTION: web DATABASE_URL: postgresql://affine:affine@localhost:5432/affine REDIS_SERVER_HOST: localhost services: @@ -511,6 +510,66 @@ jobs: name: affine fail_ci_if_error: false + server-e2e-test: + # the new version of server e2e test should be super fast, so sharding testing is not needed + name: Server E2E Test + runs-on: ubuntu-latest + needs: + - optimize_ci + - build-server-native + if: needs.optimize_ci.outputs.skip == 'false' + env: + NODE_ENV: test + DATABASE_URL: postgresql://affine:affine@localhost:5432/affine + REDIS_SERVER_HOST: localhost + services: + postgres: + image: pgvector/pgvector:pg16 + env: + POSTGRES_PASSWORD: affine + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + redis: + image: redis + ports: + - 6379:6379 + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: ./.github/actions/setup-node + with: + electron-install: false + full-cache: true + + - name: Download server-native.node + uses: actions/download-artifact@v4 + with: + name: server-native.node + path: ./packages/backend/server + + - name: Prepare Server Test Environment + uses: ./.github/actions/server-test-env + + - name: Run server tests + run: yarn affine @affine/server e2e:coverage --forbid-only + env: + COPILOT_OPENAI_API_KEY: 'use_fake_openai_api_key' + + - name: Upload server test coverage results + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./packages/backend/server/.coverage/lcov.info + flags: server-test + name: affine + fail_ci_if_error: false + rust-test: name: Run native tests runs-on: ubuntu-latest @@ -702,7 +761,7 @@ jobs: fal-key: ${{ secrets.COPILOT_FAL_API_KEY }} perplexity-key: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }} - server-e2e-test: + cloud-e2e-test: name: ${{ matrix.tests.name }} runs-on: ubuntu-latest needs: @@ -719,16 +778,16 @@ jobs: fail-fast: false matrix: tests: - - name: 'Server E2E Test 1/3' + - name: 'Cloud E2E Test 1/3' shard: 1 script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=1/3 - - name: 'Server E2E Test 2/3' + - name: 'Cloud E2E Test 2/3' shard: 2 script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=2/3 - - name: 'Server E2E Test 3/3' + - name: 'Cloud E2E Test 3/3' shard: 3 script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=3/3 - - name: 'Server Desktop E2E Test' + - name: 'Cloud Desktop E2E Test' shard: desktop script: | yarn affine @affine/electron build:dev @@ -951,11 +1010,12 @@ jobs: - build-electron-renderer - native-unit-test - server-test + - server-e2e-test - rust-test - copilot-api-test - copilot-e2e-test - - server-e2e-test - desktop-test + - cloud-e2e-test - test-build-mobile-app if: always() runs-on: ubuntu-latest diff --git a/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch b/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch new file mode 100644 index 0000000000..6de89f671e --- /dev/null +++ b/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch @@ -0,0 +1,36 @@ +diff --git a/testing-module.d.ts b/testing-module.d.ts +index 0b08dfe24534c605f58f2104255eb2a20c3fb566..8fad3ab267decccca2d4d9a8e208ace2cd811e92 100644 +--- a/testing-module.d.ts ++++ b/testing-module.d.ts +@@ -13,6 +13,7 @@ export declare class TestingModule extends NestApplicationContext { + protected readonly graphInspector: GraphInspector; + constructor(container: NestContainer, graphInspector: GraphInspector, contextModule: Module, applicationConfig: ApplicationConfig, scope?: Type[]); + private isHttpServer; ++ useCustomApplicationConstructor(Ctor: Type): void; + createNestApplication(httpAdapter: HttpServer | AbstractHttpAdapter, options?: NestApplicationOptions): T; + createNestApplication(options?: NestApplicationOptions): T; + createNestMicroservice(options: NestMicroserviceOptions & T): INestMicroservice; +diff --git a/testing-module.js b/testing-module.js +index 17957b409b224bc43c7e40a1071cf08061366063..6bc4e8a694fdec02df91e512131ffd70259d8859 100644 +--- a/testing-module.js ++++ b/testing-module.js +@@ -15,6 +15,9 @@ class TestingModule extends core_1.NestApplicationContext { + this.applicationConfig = applicationConfig; + this.graphInspector = graphInspector; + } ++ useCustomApplicationConstructor(Ctor) { ++ this.applicationConstructor = Ctor; ++ } + isHttpServer(serverOrOptions) { + return !!(serverOrOptions && serverOrOptions.patch); + } +@@ -24,7 +27,8 @@ class TestingModule extends core_1.NestApplicationContext { + : [this.createHttpAdapter(), serverOrOptions]; + this.applyLogger(appOptions); + this.container.setHttpAdapter(httpAdapter); +- const instance = new core_1.NestApplication(this.container, httpAdapter, this.applicationConfig, this.graphInspector, appOptions); ++ const Ctor = this.applicationConstructor || core_1.NestApplication; ++ const instance = new Ctor(this.container, httpAdapter, this.applicationConfig, this.graphInspector, appOptions); + return this.createAdapterProxy(instance, httpAdapter); + } + createNestMicroservice(options) { diff --git a/packages/backend/server/ava.config.js b/packages/backend/server/ava.config.js new file mode 100644 index 0000000000..817479849a --- /dev/null +++ b/packages/backend/server/ava.config.js @@ -0,0 +1,31 @@ +const newE2E = process.env.TEST_MODE === 'e2e'; +const newE2ETests = './src/__tests__/e2e/**/*.spec.ts'; + +const preludes = ['./src/prelude.ts']; + +if (newE2E) { + preludes.push('./src/__tests__/e2e/prelude.ts'); +} + +export default { + timeout: '1m', + extensions: { + ts: 'module', + }, + watchMode: { + ignoreChanges: ['**/*.gen.*'], + }, + files: newE2E + ? [newE2ETests] + : ['**/*.spec.ts', '**/*.e2e.ts', '!' + newE2ETests], + require: preludes, + environmentVariables: { + NODE_ENV: 'test', + DEPLOYMENT_TYPE: 'affine', + MAILER_HOST: '0.0.0.0', + MAILER_PORT: '1025', + MAILER_USER: 'noreply@toeverything.info', + MAILER_PASSWORD: 'affine', + MAILER_SENDER: 'noreply@toeverything.info', + }, +}; diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 7ad8a6ae07..632da0ca13 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -15,6 +15,8 @@ "test:copilot": "ava \"src/__tests__/**/copilot-*.spec.ts\"", "test:coverage": "c8 ava --concurrency 1 --serial", "test:copilot:coverage": "c8 ava --timeout=5m \"src/__tests__/**/copilot-*.spec.ts\"", + "e2e": "cross-env TEST_MODE=e2e ava", + "e2e:coverage": "cross-env TEST_MODE=e2e c8 ava", "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", @@ -111,7 +113,7 @@ "@affine-tools/utils": "workspace:*", "@affine/server-native": "workspace:*", "@faker-js/faker": "^9.6.0", - "@nestjs/testing": "^10.4.15", + "@nestjs/testing": "patch:@nestjs/testing@npm%3A10.4.15#~/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch", "@types/cookie-parser": "^1.4.8", "@types/express": "^4.17.21", "@types/graphql-upload": "^17.0.0", @@ -134,39 +136,6 @@ "sinon": "^19.0.2", "supertest": "^7.0.0" }, - "ava": { - "timeout": "1m", - "extensions": { - "ts": "module" - }, - "workerThreads": false, - "nodeArguments": [ - "--trace-sigint" - ], - "watchMode": { - "ignoreChanges": [ - "static/**", - "**/*.gen.*" - ] - }, - "files": [ - "**/__tests__/**/*.spec.ts", - "**/__tests__/**/*.e2e.ts" - ], - "require": [ - "./src/prelude.ts" - ], - "environmentVariables": { - "NODE_ENV": "test", - "MAILER_HOST": "0.0.0.0", - "MAILER_PORT": "1025", - "MAILER_USER": "noreply@toeverything.info", - "MAILER_PASSWORD": "affine", - "MAILER_SENDER": "noreply@toeverything.info", - "FEATURES_EARLY_ACCESS_PREVIEW": "false", - "DEPLOYMENT_TYPE": "affine" - } - }, "nodemonConfig": { "exec": "node", "ignore": [ @@ -185,7 +154,7 @@ }, "c8": { "reporter": [ - "text", + "text-summary", "lcov" ], "report-dir": ".coverage", diff --git a/packages/backend/server/src/__tests__/create-module.ts b/packages/backend/server/src/__tests__/create-module.ts new file mode 100644 index 0000000000..3d7e40d283 --- /dev/null +++ b/packages/backend/server/src/__tests__/create-module.ts @@ -0,0 +1,49 @@ +import { ModuleMetadata } from '@nestjs/common'; +import { + Test, + TestingModule as NestjsTestingModule, + TestingModuleBuilder, +} from '@nestjs/testing'; + +import { FunctionalityModules } from '../app.module'; +import { AFFiNELogger } from '../base'; +import { TEST_LOG_LEVEL } from './utils'; + +interface TestingModuleMetadata extends ModuleMetadata { + tapModule?(m: TestingModuleBuilder): void; +} + +export interface TestingModule extends NestjsTestingModule { + [Symbol.asyncDispose](): Promise; +} + +export async function createModule( + metadata: TestingModuleMetadata +): Promise { + const { tapModule, ...meta } = metadata; + + const builder = Test.createTestingModule({ + ...meta, + imports: [...FunctionalityModules, ...(meta.imports ?? [])], + }); + + // when custom override happens + if (tapModule) { + tapModule(builder); + } + + const module = (await builder.compile()) as TestingModule; + + const logger = new AFFiNELogger(); + // we got a lot smoking tests try to break nestjs + // can't tolerate the noisy logs + logger.setLogLevels([TEST_LOG_LEVEL]); + module.useLogger(logger); + + await module.init(); + module[Symbol.asyncDispose] = async () => { + await module.close(); + }; + + return module; +} diff --git a/packages/backend/server/src/__tests__/e2e/app.spec.ts b/packages/backend/server/src/__tests__/e2e/app.spec.ts new file mode 100644 index 0000000000..7c808c0b6f --- /dev/null +++ b/packages/backend/server/src/__tests__/e2e/app.spec.ts @@ -0,0 +1,5 @@ +import { app, e2e } from './test'; + +e2e('should create test app correctly', async t => { + t.truthy(app); +}); diff --git a/packages/backend/server/src/__tests__/e2e/create-app.ts b/packages/backend/server/src/__tests__/e2e/create-app.ts new file mode 100644 index 0000000000..714d4a9216 --- /dev/null +++ b/packages/backend/server/src/__tests__/e2e/create-app.ts @@ -0,0 +1,79 @@ +import { INestApplication } from '@nestjs/common'; +import { NestApplication } from '@nestjs/core'; +import { Test, TestingModuleBuilder } from '@nestjs/testing'; +import cookieParser from 'cookie-parser'; +import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; + +import { + AFFiNELogger, + CacheInterceptor, + CloudThrottlerGuard, + GlobalExceptionFilter, + OneMB, +} from '../../base'; +import { SocketIoAdapter } from '../../base/websocket'; +import { AuthGuard } from '../../core/auth'; +import { TEST_LOG_LEVEL } from '../utils'; + +interface TestingAppMetadata { + tapModule?(m: TestingModuleBuilder): void; + tapApp?(app: INestApplication): void; +} + +export class TestingApp extends NestApplication { + async [Symbol.asyncDispose]() { + await this.close(); + } +} + +export async function createApp( + metadata: TestingAppMetadata = {} +): Promise { + const { buildAppModule } = await import('../../app.module'); + const { tapModule, tapApp } = metadata; + + const builder = Test.createTestingModule({ + imports: [buildAppModule()], + }); + + // when custom override happens + if (tapModule) { + tapModule(builder); + } + + const module = await builder.compile(); + + module.useCustomApplicationConstructor(TestingApp); + + const app = module.createNestApplication({ + cors: true, + bodyParser: true, + rawBody: true, + }); + + const logger = new AFFiNELogger(); + logger.setLogLevels([TEST_LOG_LEVEL]); + app.useLogger(logger); + app.use(cookieParser()); + app.useBodyParser('raw', { limit: 1 * OneMB }); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * OneMB, + maxFiles: 5, + }) + ); + + app.useGlobalGuards(app.get(AuthGuard), app.get(CloudThrottlerGuard)); + app.useGlobalInterceptors(app.get(CacheInterceptor)); + app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter())); + + const adapter = new SocketIoAdapter(app); + app.useWebSocketAdapter(adapter); + app.enableShutdownHooks(); + + if (tapApp) { + tapApp(app); + } + + return app; +} diff --git a/packages/backend/server/src/__tests__/e2e/prelude.ts b/packages/backend/server/src/__tests__/e2e/prelude.ts new file mode 100644 index 0000000000..e63a0e5934 --- /dev/null +++ b/packages/backend/server/src/__tests__/e2e/prelude.ts @@ -0,0 +1,4 @@ +import { createApp } from './create-app'; + +// @ts-expect-error testing +globalThis.app = await createApp(); diff --git a/packages/backend/server/src/__tests__/e2e/test.ts b/packages/backend/server/src/__tests__/e2e/test.ts new file mode 100644 index 0000000000..a35938c8e4 --- /dev/null +++ b/packages/backend/server/src/__tests__/e2e/test.ts @@ -0,0 +1,9 @@ +import test, { registerCompletionHandler } from 'ava'; + +export const e2e = test; +// @ts-expect-error created in prelude.ts +export const app = globalThis.app; + +registerCompletionHandler(() => { + app.close(); +}); diff --git a/packages/backend/server/src/__tests__/utils/testing-module.ts b/packages/backend/server/src/__tests__/utils/testing-module.ts index c02e517750..a784f53b69 100644 --- a/packages/backend/server/src/__tests__/utils/testing-module.ts +++ b/packages/backend/server/src/__tests__/utils/testing-module.ts @@ -13,6 +13,9 @@ import { AFFiNELogger, Runtime } from '../../base'; import { GqlModule } from '../../base/graphql'; import { AuthGuard, AuthModule } from '../../core/auth'; import { ModelsModule } from '../../models'; +// for jsdoc inference +// oxlint-disable-next-line no-unused-vars +import type { createModule } from '../create-module'; import { createFactory } from '../mocks'; import { initTestingDB, TEST_LOG_LEVEL } from './utils'; @@ -48,6 +51,9 @@ class MockResolver { } } +/** + * @deprecated use {@link createModule} instead + */ export async function createTestingModule( moduleDef: TestingModuleMeatdata = {}, autoInitialize = true diff --git a/yarn.lock b/yarn.lock index e426b9733d..d808a24c5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -861,7 +861,7 @@ __metadata: "@nestjs/platform-express": "npm:^10.4.15" "@nestjs/platform-socket.io": "npm:^10.4.15" "@nestjs/schedule": "npm:^4.1.2" - "@nestjs/testing": "npm:^10.4.15" + "@nestjs/testing": "patch:@nestjs/testing@npm%3A10.4.15#~/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch" "@nestjs/throttler": "npm:6.4.0" "@nestjs/websockets": "npm:^10.4.15" "@node-rs/argon2": "npm:^2.0.2" @@ -8194,7 +8194,7 @@ __metadata: languageName: node linkType: hard -"@nestjs/testing@npm:^10.4.15": +"@nestjs/testing@npm:10.4.15": version: 10.4.15 resolution: "@nestjs/testing@npm:10.4.15" dependencies: @@ -8213,6 +8213,25 @@ __metadata: languageName: node linkType: hard +"@nestjs/testing@patch:@nestjs/testing@npm%3A10.4.15#~/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch": + version: 10.4.15 + resolution: "@nestjs/testing@patch:@nestjs/testing@npm%3A10.4.15#~/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch::version=10.4.15&hash=9a94fb" + dependencies: + tslib: "npm:2.8.1" + peerDependencies: + "@nestjs/common": ^10.0.0 + "@nestjs/core": ^10.0.0 + "@nestjs/microservices": ^10.0.0 + "@nestjs/platform-express": ^10.0.0 + peerDependenciesMeta: + "@nestjs/microservices": + optional: true + "@nestjs/platform-express": + optional: true + checksum: 10/0eb2fcfda4515c66b31820f1cf426f0bd4ba38bbf21fbbac35886943787bbb8f59edb0f4301723913de34831e19237c4d2878a5f3a60c26e2d0cc55dd2e870de + languageName: node + linkType: hard + "@nestjs/throttler@npm:6.4.0": version: 6.4.0 resolution: "@nestjs/throttler@npm:6.4.0"