feat: cron job for copilot test

This commit is contained in:
DarkSky
2024-11-18 16:41:05 +08:00
parent 55fffe0762
commit 6f3f291b18
19 changed files with 471 additions and 96 deletions
@@ -0,0 +1,9 @@
import packageJson from '../package.json' with { type: 'json' };
export default {
...packageJson.ava,
environmentVariables: {
...packageJson.ava.environmentVariables,
TS_NODE_PROJECT: './tests/tsconfig.docker.json',
},
};
@@ -1,10 +1,10 @@
import { randomUUID } from 'node:crypto';
import { randomInt, randomUUID } from 'node:crypto';
import { createRandomAIUser } from '@affine-test/kit/utils/cloud';
import { hash } from '@node-rs/argon2';
import type { ExecutionContext, TestFn } from 'ava';
import ava from 'ava';
import { z } from 'zod';
import { createWorkspace } from './utils';
import {
chatWithImages,
chatWithText,
@@ -15,6 +15,7 @@ import {
ProviderWorkflowTestCase,
sse2array,
} from './utils/copilot';
import { createWorkspace } from './utils/workspace';
type Tester = {
app: any;
@@ -47,7 +48,7 @@ const runIfCopilotConfigured = test.macro(
}
);
export const runPrisma = async <T>(
const runPrisma = async <T>(
cb: (
prisma: InstanceType<
typeof import('../../node_modules/@prisma/client').PrismaClient
@@ -64,14 +65,99 @@ export const runPrisma = async <T>(
}
};
const cloudUserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
password: z.string(),
});
function randomName() {
return Array.from({ length: 10 }, () =>
String.fromCharCode(randomInt(65, 90))
)
.join('')
.toLowerCase();
}
async function createRandomAIUser(): Promise<{
name: string;
email: string;
password: string;
id: string;
sessionId: string;
}> {
const name = randomName();
const email = `${name}@affine.fail`;
const user = { name, email, password: '123456' };
const result = await runPrisma(async client => {
const freeFeatureId = await client.feature
.findFirst({
where: { feature: 'free_plan_v1' },
select: { id: true },
orderBy: { version: 'desc' },
})
.then(f => f!.id);
const aiFeatureId = await client.feature
.findFirst({
where: { feature: 'unlimited_copilot' },
select: { id: true },
orderBy: { version: 'desc' },
})
.then(f => f!.id);
const { id: userId } = await client.user.create({
data: {
...user,
emailVerifiedAt: new Date(),
password: await hash(user.password),
features: {
create: [
{
reason: 'created by test case',
activated: true,
featureId: freeFeatureId,
},
{
reason: 'created by test case',
activated: true,
featureId: aiFeatureId,
},
],
},
},
});
const { id: sessionId } = await client.session.create({ data: {} });
await client.userSession.create({
data: {
sessionId,
userId,
// half an hour
expiresAt: new Date(Date.now() + 60 * 30 * 1000),
},
});
return await client.user
.findUnique({
where: {
email: user.email,
},
})
.then(r => ({ ...r, sessionId }));
});
cloudUserSchema.parse(result);
return {
...result,
password: user.password,
} as any;
}
test.before(async t => {
if (!isCopilotConfigured) return;
const { endpoint } = e2eConfig;
const { email, sessionId: token } = await createRandomAIUser(
'affine.fail',
runPrisma
);
const { email, sessionId: token } = await createRandomAIUser();
const app = { getHttpServer: () => endpoint } as any;
const { id } = await createWorkspace(app, token);
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2024",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"rootDir": ".",
"outDir": "../../dist/tests",
"verbatimModuleSyntax": false,
"tsBuildInfoFile": "../../dist/tests/.tsbuildinfo"
},
"include": [".", "utils"],
"exclude": [],
"ts-node": {
"esm": true,
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node"
}
}
}
@@ -1,7 +1,7 @@
import type { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { gql } from './common';
import { gqlEndpoint } from './common';
export async function listBlobs(
app: INestApplication,
@@ -9,7 +9,7 @@ export async function listBlobs(
workspaceId: string
): Promise<string[]> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -29,7 +29,7 @@ export async function getWorkspaceBlobsSize(
workspaceId: string
): Promise<number> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.send({
query: `
@@ -49,7 +49,7 @@ export async function collectAllBlobSizes(
token: string
): Promise<number> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.send({
query: `
@@ -66,6 +66,27 @@ export async function collectAllBlobSizes(
return res.body.data.currentUser.quotaUsage.storageQuota;
}
export async function checkBlobSize(
app: INestApplication,
token: string,
workspaceId: string,
size: number
): Promise<number> {
const res = await request(app.getHttpServer())
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.send({
query: `query checkBlobSize($workspaceId: String!, $size: SafeInt!) {
checkBlobSize(workspaceId: $workspaceId, size: $size) {
size
}
}`,
variables: { workspaceId, size },
})
.expect(200);
return res.body.data.checkBlobSize.size;
}
export async function setBlob(
app: INestApplication,
token: string,
@@ -73,7 +94,7 @@ export async function setBlob(
buffer: Buffer
): Promise<string> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.field(
@@ -1 +1,37 @@
export const gql = '/graphql';
import { INestApplication } from '@nestjs/common';
import type { Response } from 'supertest';
import supertest from 'supertest';
export function handleGraphQLError(resp: Response) {
const { errors } = resp.body;
if (errors) {
const cause = errors[0];
const stacktrace = cause.extensions?.stacktrace;
throw new Error(
stacktrace
? Array.isArray(stacktrace)
? stacktrace.join('\n')
: String(stacktrace)
: cause.message,
cause
);
}
}
export const gqlEndpoint = '/graphql';
export function gql(app: INestApplication, query?: string) {
const req = supertest(app.getHttpServer())
.post(gqlEndpoint)
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' });
if (query) {
return req.send({ query });
}
return req;
}
export async function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@@ -27,8 +27,7 @@ import {
WorkflowNodeType,
WorkflowParams,
} from '../../plugins/copilot/workflow/types';
import { gql } from './common';
import { handleGraphQLError, sleep } from './utils';
import { gqlEndpoint, handleGraphQLError, sleep } from './common';
// @ts-expect-error no error
export class MockCopilotTestProvider
@@ -167,7 +166,7 @@ export async function createCopilotSession(
promptName: string
): Promise<string> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -192,7 +191,7 @@ export async function updateCopilotSession(
promptName: string
): Promise<string> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -219,7 +218,7 @@ export async function forkCopilotSession(
latestMessageId: string
): Promise<string> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -249,7 +248,7 @@ export async function createCopilotMessage(
params?: Record<string, string>
): Promise<string> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -403,7 +402,7 @@ export async function getHistories(
}
): Promise<History[]> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -3,3 +3,4 @@ export * from './invite';
export * from './user';
export * from './utils';
export * from './workspace';
export * from './common';
@@ -2,7 +2,7 @@ import type { INestApplication } from '@nestjs/common';
import request from 'supertest';
import type { InvitationType } from '../../core/workspaces';
import { gql } from './common';
import { gqlEndpoint } from './common';
export async function inviteUser(
app: INestApplication,
@@ -12,7 +12,7 @@ export async function inviteUser(
sendInviteMail = false
): Promise<string> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -37,7 +37,7 @@ export async function inviteUsers(
sendInviteMail = false
): Promise<Array<{ email: string; inviteId?: string; sentSuccess?: boolean }>> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -69,7 +69,7 @@ export async function getInviteLink(
workspaceId: string
): Promise<{ link: string; expireTime: string }> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -98,7 +98,7 @@ export async function createInviteLink(
expireTime: 'OneDay' | 'ThreeDays' | 'OneWeek' | 'OneMonth'
): Promise<{ link: string; expireTime: string }> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -124,7 +124,7 @@ export async function revokeInviteLink(
workspaceId: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -149,7 +149,7 @@ export async function acceptInviteById(
token: string = ''
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.auth(token, { type: 'bearer' })
.send({
@@ -175,7 +175,7 @@ export async function approveMember(
userId: string
): Promise<string> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.auth(token, { type: 'bearer' })
.send({
@@ -201,7 +201,7 @@ export async function leaveWorkspace(
sendLeaveMail = false
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -225,7 +225,7 @@ export async function revokeUser(
userId: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -250,7 +250,7 @@ export async function getInviteInfo(
inviteId: string
): Promise<InvitationType> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -9,7 +9,7 @@ import {
import { sessionUser } from '../../core/auth/service';
import { UserType } from '../../core/user';
import { Models } from '../../models';
import { gql } from './common';
import { gqlEndpoint } from './common';
export type UserAuthedType = UserType & { token: ClientTokenType };
@@ -69,7 +69,7 @@ export async function signUp(
export async function currentUser(app: INestApplication, token: string) {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -93,7 +93,7 @@ export async function sendChangeEmail(
callbackUrl: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -115,7 +115,7 @@ export async function sendSetPasswordEmail(
callbackUrl: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -137,7 +137,7 @@ export async function changePassword(
password: string
): Promise<string> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
@@ -159,7 +159,7 @@ export async function sendVerifyChangeEmail(
callbackUrl: string
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -181,7 +181,7 @@ export async function changeEmail(
email: string
): Promise<UserAuthedType> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(userToken, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -5,8 +5,6 @@ import { Test, TestingModuleBuilder } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import cookieParser from 'cookie-parser';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import type { Response } from 'supertest';
import supertest from 'supertest';
import { AppModule, FunctionalityModules } from '../../app.module';
import { GlobalExceptionFilter, Runtime } from '../../base';
@@ -156,35 +154,3 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
app,
};
}
export function handleGraphQLError(resp: Response) {
const { errors } = resp.body;
if (errors) {
const cause = errors[0];
const stacktrace = cause.extensions?.stacktrace;
throw new Error(
stacktrace
? Array.isArray(stacktrace)
? stacktrace.join('\n')
: String(stacktrace)
: cause.message,
cause
);
}
}
export function gql(app: INestApplication, query?: string) {
const req = supertest(app.getHttpServer())
.post('/graphql')
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' });
if (query) {
return req.send({ query });
}
return req;
}
export async function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@@ -2,7 +2,7 @@ import type { INestApplication } from '@nestjs/common';
import request from 'supertest';
import type { WorkspaceType } from '../../core/workspaces';
import { gql } from './common';
import { gqlEndpoint } from './common';
import { PermissionEnum } from './utils';
export async function createWorkspace(
@@ -10,7 +10,7 @@ export async function createWorkspace(
token: string
): Promise<WorkspaceType> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.field(
@@ -37,7 +37,7 @@ export async function getWorkspacePublicPages(
workspaceId: string
) {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -64,7 +64,7 @@ export async function getWorkspace(
take = 8
): Promise<WorkspaceType> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -87,7 +87,7 @@ export async function updateWorkspace(
isPublic: boolean
): Promise<boolean> {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -110,7 +110,7 @@ export async function publishPage(
pageId: string
) {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -134,7 +134,7 @@ export async function revokePublicPage(
pageId: string
) {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
@@ -160,7 +160,7 @@ export async function grantMember(
permission: PermissionEnum
) {
const res = await request(app.getHttpServer())
.post(gql)
.post(gqlEndpoint)
.auth(token, { type: 'bearer' })
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({