diff --git a/.github/helm/affine/charts/graphql/templates/copilot-test.yaml b/.github/helm/affine/charts/graphql/templates/copilot-test.yaml index 287ca7bc72dab..2a446a15ca3c0 100644 --- a/.github/helm/affine/charts/graphql/templates/copilot-test.yaml +++ b/.github/helm/affine/charts/graphql/templates/copilot-test.yaml @@ -1,3 +1,4 @@ +{{ if .Values.app.copilot.enabled }} apiVersion: batch/v1 kind: CronJob metadata: @@ -27,9 +28,27 @@ spec: secretKeyRef: name: pg-postgresql key: postgres-password + - name: DATABASE_URL + value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.url }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }} + - name: COPILOT_OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: "{{ .Values.app.copilot.secretName }}" + key: openaiSecret + - name: COPILOT_FAL_API_KEY + valueFrom: + secretKeyRef: + name: "{{ .Values.app.copilot.secretName }}" + key: falSecret + - name: COPILOT_UNSPLASH_API_KEY + valueFrom: + secretKeyRef: + name: "{{ .Values.app.copilot.secretName }}" + key: unsplashSecret resources: requests: cpu: '100m' memory: '200Mi' restartPolicy: Never backoffLimit: 1 +{{ end }} diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 92c59765bc503..7c1ea529fbf9e 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -12,11 +12,12 @@ "start": "node --loader ts-node/esm/transpile-only.mjs ./src/index.ts", "dev": "nodemon ./src/index.ts", "test": "ava --concurrency 1 --serial", - "test:copilot:e2e": "ava \"e2e/copilot.e2e.ts\"", + "test:copilot:e2e": "ava \"tests/**/copilot-*.e2e.ts\"", "test:copilot:spec": "ava \"tests/**/copilot-*.spec.ts\"", "test:coverage": "c8 ava --concurrency 1 --serial", - "test:copilot:e2e:coverage": "c8 ava --timeout=5m \"e2e/copilot.e2e.ts\"", + "test:copilot:e2e:coverage": "c8 ava --timeout=5m \"tests/**/copilot-*.e2e.ts\"", "test:copilot:spec:coverage": "c8 ava --timeout=5m \"tests/**/copilot-*.spec.ts\"", + "test:copilot:e2e:cron": "ava --config \"tests/ava.docker.config.js\" \"tests/**/copilot-*.e2e.ts\"", "postinstall": "prisma generate", "data-migration": "node --loader ts-node/esm/transpile-only.mjs ./src/data/index.ts", "predeploy": "yarn prisma migrate deploy && node --import ./scripts/register.js ./dist/data/index.js run", @@ -89,6 +90,7 @@ "ses": "^1.4.1", "socket.io": "^4.7.5", "stripe": "^17.0.0", + "supertest": "^7.0.0", "ts-node": "^10.9.2", "typescript": "^5.6.3", "yjs": "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch", @@ -112,8 +114,7 @@ "@types/supertest": "^6.0.2", "c8": "^10.0.0", "nodemon": "^3.1.0", - "sinon": "^19.0.0", - "supertest": "^7.0.0" + "sinon": "^19.0.0" }, "ava": { "timeout": "1m", diff --git a/packages/backend/server/tests/ava.docker.config.js b/packages/backend/server/tests/ava.docker.config.js new file mode 100644 index 0000000000000..832ff81f3b290 --- /dev/null +++ b/packages/backend/server/tests/ava.docker.config.js @@ -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', + }, +}; diff --git a/packages/backend/server/tests/copilot-provider.e2e.ts b/packages/backend/server/tests/copilot-provider.e2e.ts index 3002d4116c733..ae10f645e0ab9 100644 --- a/packages/backend/server/tests/copilot-provider.e2e.ts +++ b/packages/backend/server/tests/copilot-provider.e2e.ts @@ -5,7 +5,6 @@ import type { ExecutionContext, TestFn } from 'ava'; import ava from 'ava'; import { z } from 'zod'; -import { createWorkspace } from './utils'; import { chatWithImages, chatWithText, @@ -16,6 +15,7 @@ import { ProviderWorkflowTestCase, sse2array, } from './utils/copilot'; +import { createWorkspace } from './utils/workspace'; type Tester = { app: any; diff --git a/packages/backend/server/tests/tsconfig.docker.json b/packages/backend/server/tests/tsconfig.docker.json new file mode 100644 index 0000000000000..023ed91e8add1 --- /dev/null +++ b/packages/backend/server/tests/tsconfig.docker.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "rootDir": ".", + "outDir": "../lib/tests", + "verbatimModuleSyntax": false, + "tsBuildInfoFile": "../lib/tests/.tsbuildinfo" + }, + "include": [".", "utils"], + "exclude": [], + "ts-node": { + "esm": true, + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Node" + } + } +} diff --git a/packages/backend/server/tests/utils/blobs.ts b/packages/backend/server/tests/utils/blobs.ts index 514fbeee468d1..7bad270749fce 100644 --- a/packages/backend/server/tests/utils/blobs.ts +++ b/packages/backend/server/tests/utils/blobs.ts @@ -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 { 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 { 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 { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .send({ query: ` @@ -71,7 +71,7 @@ export async function checkBlobSize( size: number ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .send({ query: `query checkBlobSize($workspaceId: String!, $size: SafeInt!) { @@ -92,7 +92,7 @@ export async function setBlob( buffer: Buffer ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .field( diff --git a/packages/backend/server/tests/utils/common.ts b/packages/backend/server/tests/utils/common.ts index 772922367612e..444db09c806f8 100644 --- a/packages/backend/server/tests/utils/common.ts +++ b/packages/backend/server/tests/utils/common.ts @@ -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 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 const gqlEndpoint = '/graphql'; + +export async function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/packages/backend/server/tests/utils/copilot.ts b/packages/backend/server/tests/utils/copilot.ts index f57db578bc03f..780969d789766 100644 --- a/packages/backend/server/tests/utils/copilot.ts +++ b/packages/backend/server/tests/utils/copilot.ts @@ -27,8 +27,7 @@ import { WorkflowNodeType, WorkflowParams, } from '../../src/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 { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -194,7 +193,7 @@ export async function forkCopilotSession( latestMessageId: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -224,7 +223,7 @@ export async function createCopilotMessage( params?: Record ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -366,7 +365,7 @@ export async function getHistories( } ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ diff --git a/packages/backend/server/tests/utils/invite.ts b/packages/backend/server/tests/utils/invite.ts index 3bf0a3853bb34..5ea0cc43785a5 100644 --- a/packages/backend/server/tests/utils/invite.ts +++ b/packages/backend/server/tests/utils/invite.ts @@ -2,7 +2,7 @@ import type { INestApplication } from '@nestjs/common'; import request from 'supertest'; import type { InvitationType } from '../../src/core/workspaces'; -import { gql } from './common'; +import { gqlEndpoint } from './common'; export async function inviteUser( app: INestApplication, @@ -13,7 +13,7 @@ export async function inviteUser( sendInviteMail = false ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -34,7 +34,7 @@ export async function acceptInviteById( sendAcceptMail = false ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ query: ` @@ -54,7 +54,7 @@ export async function leaveWorkspace( sendLeaveMail = false ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -75,7 +75,7 @@ export async function revokeUser( userId: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -95,7 +95,7 @@ export async function getInviteInfo( inviteId: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ diff --git a/packages/backend/server/tests/utils/user.ts b/packages/backend/server/tests/utils/user.ts index f364463be99f6..82d06b1b268e4 100644 --- a/packages/backend/server/tests/utils/user.ts +++ b/packages/backend/server/tests/utils/user.ts @@ -8,7 +8,7 @@ import { } from '../../src/core/auth'; import { sessionUser } from '../../src/core/auth/service'; import { UserService, type UserType } from '../../src/core/user'; -import { gql } from './common'; +import { gqlEndpoint } from './common'; export async function internalSignIn(app: INestApplication, userId: string) { const auth = app.get(AuthService); @@ -66,7 +66,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({ @@ -90,7 +90,7 @@ export async function sendChangeEmail( callbackUrl: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -112,7 +112,7 @@ export async function sendSetPasswordEmail( callbackUrl: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -134,7 +134,7 @@ export async function changePassword( password: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ query: ` @@ -156,7 +156,7 @@ export async function sendVerifyChangeEmail( callbackUrl: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -178,7 +178,7 @@ export async function changeEmail( email: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ diff --git a/packages/backend/server/tests/utils/utils.ts b/packages/backend/server/tests/utils/utils.ts index b685b6d79d289..c8adef2f08ef2 100644 --- a/packages/backend/server/tests/utils/utils.ts +++ b/packages/backend/server/tests/utils/utils.ts @@ -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 '../../src/app.module'; import { AuthGuard, AuthModule } from '../../src/core/auth'; @@ -152,35 +150,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)); -} diff --git a/packages/backend/server/tests/utils/workspace.ts b/packages/backend/server/tests/utils/workspace.ts index f895d07c2686c..b647eb108aaeb 100644 --- a/packages/backend/server/tests/utils/workspace.ts +++ b/packages/backend/server/tests/utils/workspace.ts @@ -2,14 +2,14 @@ import type { INestApplication } from '@nestjs/common'; import request from 'supertest'; import type { WorkspaceType } from '../../src/core/workspaces'; -import { gql } from './common'; +import { gqlEndpoint } from './common'; export async function createWorkspace( app: INestApplication, token: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .field( @@ -36,7 +36,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({ @@ -63,7 +63,7 @@ export async function getWorkspace( take = 8 ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -86,7 +86,7 @@ export async function updateWorkspace( isPublic: boolean ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -109,7 +109,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({ @@ -133,7 +133,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({