refactor(server): e2e utilities (#11000)

This commit is contained in:
forehalo committed 2025-03-19 17:00:20 +00:00
1 parent 21c4a29f55
commit f889886b31
153 files changed
+214 -32

No files matched your search

+1 -1
View File
@@ -24,13 +24,13 @@ test-results
**/*.gen.ts
**/*.gql
**/*.d.ts
packages/frontend/graphql/src/graphql/index.ts
# per files
tools/cli/src/webpack/error-handler.js
packages/backend/native/index.d.ts
packages/backend/server/src/__tests__/__snapshots__
packages/common/native/fixtures/**
packages/common/graphql/src/graphql/index.ts
packages/frontend/native/index.d.ts
packages/frontend/native/index.js
packages/frontend/apps/android/App/app/build/**
+1 -1
View File
@@ -27,11 +27,11 @@
"**/*.gen.ts",
"**/*.gql",
"**/*.d.ts",
"packages/frontend/graphql/src/graphql/index.ts",
"tools/cli/src/webpack/error-handler.js",
"packages/backend/native/index.d.ts",
"packages/backend/server/src/__tests__/__snapshots__",
"packages/common/native/fixtures/**",
"packages/common/graphql/src/graphql/index.ts",
"packages/frontend/native/index.d.ts",
"packages/frontend/native/index.js",
"packages/frontend/apps/android/App/app/build/**",
+1
View File
@@ -114,6 +114,7 @@
"devDependencies": {
"@affine-tools/cli": "workspace:*",
"@affine-tools/utils": "workspace:*",
"@affine/graphql": "workspace:*",
"@affine/server-native": "workspace:*",
"@faker-js/faker": "^9.6.0",
"@nestjs/testing": "patch:@nestjs/testing@npm%3A10.4.15#~/.yarn/patches/@nestjs-testing-npm-10.4.15-d591a1705a.patch",
@@ -1,5 +1,18 @@
import { getCurrentUserQuery } from '@affine/graphql';
import { app, e2e } from './test';
e2e('should create test app correctly', async t => {
t.truthy(app);
});
e2e('should handle http request', async t => {
const res = await app.GET('/info');
t.is(res.status, 200);
t.is(res.body.compatibility, AFFiNE.version);
});
e2e('should handle gql request', async t => {
const user = await app.gql({ query: getCurrentUserQuery });
t.is(user.currentUser, null);
});
@@ -1,8 +1,13 @@
import assert from 'node:assert';
import { gqlFetcherFactory } from '@affine/graphql';
import { INestApplication } from '@nestjs/common';
import { NestApplication } from '@nestjs/core';
import { Test, TestingModuleBuilder } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import cookieParser from 'cookie-parser';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import supertest from 'supertest';
import {
AFFiNELogger,
@@ -12,10 +17,16 @@ import {
OneMB,
} from '../../base';
import { SocketIoAdapter } from '../../base/websocket';
import { AuthGuard } from '../../core/auth';
import { AuthGuard, AuthService } from '../../core/auth';
import { Mailer } from '../../core/mail';
import { MockMailer } from '../mocks';
import { TEST_LOG_LEVEL } from '../utils';
import {
createFactory,
MockedUser,
MockMailer,
MockUser,
MockUserInput,
} from '../mocks';
import { parseCookies, TEST_LOG_LEVEL } from '../utils';
interface TestingAppMetadata {
tapModule?(m: TestingModuleBuilder): void;
@@ -23,18 +34,158 @@ interface TestingAppMetadata {
}
export class TestingApp extends NestApplication {
get mails() {
return this.get(Mailer, { strict: false }) as MockMailer;
private sessionCookie: string | null = null;
private currentUserCookie: string | null = null;
private readonly userCookies: Set<string> = new Set();
create = createFactory(this.get(PrismaClient, { strict: false }));
mails = this.get(Mailer, { strict: false }) as MockMailer;
get url() {
const server = this.getHttpServer();
if (!server.address()) {
server.listen();
}
return `http://localhost:${server.address().port}`;
}
async [Symbol.asyncDispose]() {
await this.close();
}
request(
method: 'options' | 'get' | 'post' | 'put' | 'delete' | 'patch',
path: string
): supertest.Test {
return supertest(this.getHttpServer())
[method](path)
.set('Cookie', [
`${AuthService.sessionCookieName}=${this.sessionCookie ?? ''}`,
`${AuthService.userCookieName}=${this.currentUserCookie ?? ''}`,
]);
}
gql = gqlFetcherFactory('', async (_input, init) => {
assert(init, 'no request content');
assert(init.body, 'body is required for gql request');
assert(init.headers, 'headers is required for gql request');
const res = await this.request('post', '/graphql')
.send(init?.body)
.set('accept', 'application/json')
.set(init.headers as Record<string, string>);
return new Response(Buffer.from(JSON.stringify(res.body)), {
status: res.status,
headers: res.headers,
});
});
OPTIONS(path: string): supertest.Test {
return this.request('options', path);
}
GET(path: string): supertest.Test {
return this.request('get', path);
}
POST(path: string): supertest.Test {
return this.request('post', path).on(
'response',
(res: supertest.Response) => {
const cookies = parseCookies(res);
if (AuthService.sessionCookieName in cookies) {
if (this.sessionCookie !== cookies[AuthService.sessionCookieName]) {
this.userCookies.clear();
}
this.sessionCookie = cookies[AuthService.sessionCookieName];
this.currentUserCookie = cookies[AuthService.userCookieName];
if (this.currentUserCookie) {
this.userCookies.add(this.currentUserCookie);
}
}
return res;
}
);
}
PUT(path: string): supertest.Test {
return this.request('put', path);
}
DELETE(path: string): supertest.Test {
return this.request('delete', path);
}
PATCH(path: string): supertest.Test {
return this.request('patch', path);
}
async createUser(overrides?: Partial<MockUserInput>) {
return await this.create(MockUser, overrides);
}
async signup(overrides?: Partial<MockUserInput>) {
const user = await this.create(MockUser, overrides);
await this.login(user);
return user;
}
async login(user: MockedUser) {
await this.POST('/api/auth/sign-in')
.send({
email: user.email,
password: user.password,
})
.expect(200);
}
async switchUser(userOrId: string | { id: string }) {
if (!this.sessionCookie) {
throw new Error('No user is logged in.');
}
const userId = typeof userOrId === 'string' ? userOrId : userOrId.id;
if (userId === this.currentUserCookie) {
return;
}
if (this.userCookies.has(userId)) {
this.currentUserCookie = userId;
} else {
throw new Error(`User [${userId}] is not logged in.`);
}
}
async logout(userId?: string) {
const res = await this.GET(
'/api/auth/sign-out' + (userId ? `?user_id=${userId}` : '')
).expect(200);
const cookies = parseCookies(res);
this.sessionCookie = cookies[AuthService.sessionCookieName];
if (!this.sessionCookie) {
this.currentUserCookie = null;
this.userCookies.clear();
} else {
this.currentUserCookie = cookies[AuthService.userCookieName];
if (userId) {
this.userCookies.delete(userId);
}
}
}
}
let GLOBAL_APP_INSTANCE: TestingApp | null = null;
export async function createApp(
metadata: TestingAppMetadata = {}
): Promise<TestingApp> {
if (GLOBAL_APP_INSTANCE) {
return GLOBAL_APP_INSTANCE;
}
const { buildAppModule } = await import('../../app.module');
const { tapModule, tapApp } = metadata;
@@ -81,5 +232,8 @@ export async function createApp(
tapApp(app);
}
await app.init();
GLOBAL_APP_INSTANCE = app;
return app;
}
@@ -1,4 +1,11 @@
import { getBuildConfig } from '@affine-tools/utils/build-config';
import { Package } from '@affine-tools/utils/workspace';
import { createApp } from './create-app';
globalThis.BUILD_CONFIG = getBuildConfig(new Package('@affine/web'), {
mode: 'development',
channel: 'canary',
});
// @ts-expect-error testing
globalThis.app = await createApp();
@@ -1,8 +1,10 @@
import test, { registerCompletionHandler } from 'ava';
import { type TestingApp } from './create-app';
export const e2e = test;
// @ts-expect-error created in prelude.ts
export const app = globalThis.app;
export const app: TestingApp = globalThis.app;
registerCompletionHandler(() => {
app.close();
+1
View File
@@ -14,6 +14,7 @@
"references": [
{ "path": "../../../tools/cli" },
{ "path": "../../../tools/utils" },
{ "path": "../../common/graphql" },
{ "path": "../native" }
]
}
+1
View File
@@ -1,6 +1,7 @@
{
"name": "@affine/debug",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
+1
View File
@@ -2,6 +2,7 @@
"name": "@affine/error",
"version": "0.20.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 153 files, more files were not shown because too many files have changed in this diff. Show more