test(server): new test facilities (#10870)

close CLOUD-142
This commit is contained in:
forehalo 2025-03-17 10:02:12 +00:00
parent 92db9a693a
commit 9b5d12dc71
No known key found for this signature in database
GPG Key ID: 56709255DC7EC728
12 changed files with 311 additions and 46 deletions

View File

@ -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

View File

@ -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

View File

@ -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<any>[]);
private isHttpServer;
+ useCustomApplicationConstructor(Ctor: Type<INestApplication>): void;
createNestApplication<T extends INestApplication = INestApplication>(httpAdapter: HttpServer | AbstractHttpAdapter, options?: NestApplicationOptions): T;
createNestApplication<T extends INestApplication = INestApplication>(options?: NestApplicationOptions): T;
createNestMicroservice<T extends object>(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) {

View File

@ -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',
},
};

View File

@ -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",

View File

@ -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<void>;
}
export async function createModule(
metadata: TestingModuleMetadata
): Promise<TestingModule> {
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;
}

View File

@ -0,0 +1,5 @@
import { app, e2e } from './test';
e2e('should create test app correctly', async t => {
t.truthy(app);
});

View File

@ -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<TestingApp> {
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<TestingApp>({
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;
}

View File

@ -0,0 +1,4 @@
import { createApp } from './create-app';
// @ts-expect-error testing
globalThis.app = await createApp();

View File

@ -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();
});

View File

@ -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

View File

@ -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"