refactor(server): e2e utilities (#11000)
This commit is contained in:
@@ -24,13 +24,13 @@ test-results
|
|||||||
**/*.gen.ts
|
**/*.gen.ts
|
||||||
**/*.gql
|
**/*.gql
|
||||||
**/*.d.ts
|
**/*.d.ts
|
||||||
packages/frontend/graphql/src/graphql/index.ts
|
|
||||||
|
|
||||||
# per files
|
# per files
|
||||||
tools/cli/src/webpack/error-handler.js
|
tools/cli/src/webpack/error-handler.js
|
||||||
packages/backend/native/index.d.ts
|
packages/backend/native/index.d.ts
|
||||||
packages/backend/server/src/__tests__/__snapshots__
|
packages/backend/server/src/__tests__/__snapshots__
|
||||||
packages/common/native/fixtures/**
|
packages/common/native/fixtures/**
|
||||||
|
packages/common/graphql/src/graphql/index.ts
|
||||||
packages/frontend/native/index.d.ts
|
packages/frontend/native/index.d.ts
|
||||||
packages/frontend/native/index.js
|
packages/frontend/native/index.js
|
||||||
packages/frontend/apps/android/App/app/build/**
|
packages/frontend/apps/android/App/app/build/**
|
||||||
|
|||||||
@@ -27,11 +27,11 @@
|
|||||||
"**/*.gen.ts",
|
"**/*.gen.ts",
|
||||||
"**/*.gql",
|
"**/*.gql",
|
||||||
"**/*.d.ts",
|
"**/*.d.ts",
|
||||||
"packages/frontend/graphql/src/graphql/index.ts",
|
|
||||||
"tools/cli/src/webpack/error-handler.js",
|
"tools/cli/src/webpack/error-handler.js",
|
||||||
"packages/backend/native/index.d.ts",
|
"packages/backend/native/index.d.ts",
|
||||||
"packages/backend/server/src/__tests__/__snapshots__",
|
"packages/backend/server/src/__tests__/__snapshots__",
|
||||||
"packages/common/native/fixtures/**",
|
"packages/common/native/fixtures/**",
|
||||||
|
"packages/common/graphql/src/graphql/index.ts",
|
||||||
"packages/frontend/native/index.d.ts",
|
"packages/frontend/native/index.d.ts",
|
||||||
"packages/frontend/native/index.js",
|
"packages/frontend/native/index.js",
|
||||||
"packages/frontend/apps/android/App/app/build/**",
|
"packages/frontend/apps/android/App/app/build/**",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@affine-tools/cli": "workspace:*",
|
"@affine-tools/cli": "workspace:*",
|
||||||
"@affine-tools/utils": "workspace:*",
|
"@affine-tools/utils": "workspace:*",
|
||||||
|
"@affine/graphql": "workspace:*",
|
||||||
"@affine/server-native": "workspace:*",
|
"@affine/server-native": "workspace:*",
|
||||||
"@faker-js/faker": "^9.6.0",
|
"@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",
|
"@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';
|
import { app, e2e } from './test';
|
||||||
|
|
||||||
e2e('should create test app correctly', async t => {
|
e2e('should create test app correctly', async t => {
|
||||||
t.truthy(app);
|
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 { INestApplication } from '@nestjs/common';
|
||||||
import { NestApplication } from '@nestjs/core';
|
import { NestApplication } from '@nestjs/core';
|
||||||
import { Test, TestingModuleBuilder } from '@nestjs/testing';
|
import { Test, TestingModuleBuilder } from '@nestjs/testing';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||||
|
import supertest from 'supertest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AFFiNELogger,
|
AFFiNELogger,
|
||||||
@@ -12,10 +17,16 @@ import {
|
|||||||
OneMB,
|
OneMB,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
import { SocketIoAdapter } from '../../base/websocket';
|
import { SocketIoAdapter } from '../../base/websocket';
|
||||||
import { AuthGuard } from '../../core/auth';
|
import { AuthGuard, AuthService } from '../../core/auth';
|
||||||
import { Mailer } from '../../core/mail';
|
import { Mailer } from '../../core/mail';
|
||||||
import { MockMailer } from '../mocks';
|
import {
|
||||||
import { TEST_LOG_LEVEL } from '../utils';
|
createFactory,
|
||||||
|
MockedUser,
|
||||||
|
MockMailer,
|
||||||
|
MockUser,
|
||||||
|
MockUserInput,
|
||||||
|
} from '../mocks';
|
||||||
|
import { parseCookies, TEST_LOG_LEVEL } from '../utils';
|
||||||
|
|
||||||
interface TestingAppMetadata {
|
interface TestingAppMetadata {
|
||||||
tapModule?(m: TestingModuleBuilder): void;
|
tapModule?(m: TestingModuleBuilder): void;
|
||||||
@@ -23,18 +34,158 @@ interface TestingAppMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class TestingApp extends NestApplication {
|
export class TestingApp extends NestApplication {
|
||||||
get mails() {
|
private sessionCookie: string | null = null;
|
||||||
return this.get(Mailer, { strict: false }) as MockMailer;
|
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]() {
|
async [Symbol.asyncDispose]() {
|
||||||
await this.close();
|
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(
|
export async function createApp(
|
||||||
metadata: TestingAppMetadata = {}
|
metadata: TestingAppMetadata = {}
|
||||||
): Promise<TestingApp> {
|
): Promise<TestingApp> {
|
||||||
|
if (GLOBAL_APP_INSTANCE) {
|
||||||
|
return GLOBAL_APP_INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
const { buildAppModule } = await import('../../app.module');
|
const { buildAppModule } = await import('../../app.module');
|
||||||
const { tapModule, tapApp } = metadata;
|
const { tapModule, tapApp } = metadata;
|
||||||
|
|
||||||
@@ -81,5 +232,8 @@ export async function createApp(
|
|||||||
tapApp(app);
|
tapApp(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
|
||||||
|
GLOBAL_APP_INSTANCE = app;
|
||||||
return 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';
|
import { createApp } from './create-app';
|
||||||
|
|
||||||
|
globalThis.BUILD_CONFIG = getBuildConfig(new Package('@affine/web'), {
|
||||||
|
mode: 'development',
|
||||||
|
channel: 'canary',
|
||||||
|
});
|
||||||
// @ts-expect-error testing
|
// @ts-expect-error testing
|
||||||
globalThis.app = await createApp();
|
globalThis.app = await createApp();
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import test, { registerCompletionHandler } from 'ava';
|
import test, { registerCompletionHandler } from 'ava';
|
||||||
|
|
||||||
|
import { type TestingApp } from './create-app';
|
||||||
|
|
||||||
export const e2e = test;
|
export const e2e = test;
|
||||||
// @ts-expect-error created in prelude.ts
|
// @ts-expect-error created in prelude.ts
|
||||||
export const app = globalThis.app;
|
export const app: TestingApp = globalThis.app;
|
||||||
|
|
||||||
registerCompletionHandler(() => {
|
registerCompletionHandler(() => {
|
||||||
app.close();
|
app.close();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../../tools/cli" },
|
{ "path": "../../../tools/cli" },
|
||||||
{ "path": "../../../tools/utils" },
|
{ "path": "../../../tools/utils" },
|
||||||
|
{ "path": "../../common/graphql" },
|
||||||
{ "path": "../native" }
|
{ "path": "../native" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@affine/debug",
|
"name": "@affine/debug",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "@affine/error",
|
"name": "@affine/error",
|
||||||
"version": "0.20.0",
|
"version": "0.20.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user