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
12 changed files with 311 additions and 46 deletions

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