From 6f3f291b18f760650d550543bff75dc4b875db82 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Mon, 18 Nov 2024 16:41:05 +0800 Subject: [PATCH] feat: cron job for copilot test --- .github/actions/deploy/deploy.mjs | 4 + .../graphql/templates/copilot-secret.yaml | 2 + .../graphql/templates/copilot-test.yaml | 66 ++++++++++++ .github/workflows/deploy.yml | 3 + oxlint.json | 1 + packages/backend/server/package.json | 10 +- .../server/scripts/copilot-cron-test.js | 99 +++++++++++++++++ .../server/src/__tests__/ava.docker.config.js | 9 ++ .../src/__tests__/copilot-provider.e2e.ts | 102 ++++++++++++++++-- .../server/src/__tests__/tsconfig.docker.json | 20 ++++ .../server/src/__tests__/utils/blobs.ts | 31 +++++- .../server/src/__tests__/utils/common.ts | 38 ++++++- .../server/src/__tests__/utils/copilot.ts | 13 ++- .../server/src/__tests__/utils/index.ts | 1 + .../server/src/__tests__/utils/invite.ts | 22 ++-- .../server/src/__tests__/utils/user.ts | 14 +-- .../server/src/__tests__/utils/utils.ts | 34 ------ .../server/src/__tests__/utils/workspace.ts | 16 +-- yarn.lock | 82 +++++++++++--- 19 files changed, 471 insertions(+), 96 deletions(-) create mode 100644 .github/helm/affine/charts/graphql/templates/copilot-test.yaml create mode 100644 packages/backend/server/scripts/copilot-cron-test.js create mode 100644 packages/backend/server/src/__tests__/ava.docker.config.js create mode 100644 packages/backend/server/src/__tests__/tsconfig.docker.json diff --git a/.github/actions/deploy/deploy.mjs b/.github/actions/deploy/deploy.mjs index e5a35a18bf..114198f190 100644 --- a/.github/actions/deploy/deploy.mjs +++ b/.github/actions/deploy/deploy.mjs @@ -19,6 +19,8 @@ const { COPILOT_FAL_API_KEY, COPILOT_PERPLEXITY_API_KEY, COPILOT_UNSPLASH_API_KEY, + SLACK_BOT_TOKEN, + RELEASE_SLACK_CHNNEL_ID, MAILER_SENDER, MAILER_USER, MAILER_PASSWORD, @@ -150,6 +152,8 @@ const createHelmCommand = ({ isDryRun }) => { `--set-string graphql.app.copilot.fal.key="${COPILOT_FAL_API_KEY}"`, `--set-string graphql.app.copilot.perplexity.key="${COPILOT_PERPLEXITY_API_KEY}"`, `--set-string graphql.app.copilot.unsplash.key="${COPILOT_UNSPLASH_API_KEY}"`, + `--set-string graphql.app.copilot.slack.botToken="${SLACK_BOT_TOKEN}"`, + `--set-string graphql.app.copilot.slack.channelId="${RELEASE_SLACK_CHNNEL_ID}"`, `--set-string graphql.app.mailer.sender="${MAILER_SENDER}"`, `--set-string graphql.app.mailer.user="${MAILER_USER}"`, `--set-string graphql.app.mailer.password="${MAILER_PASSWORD}"`, diff --git a/.github/helm/affine/charts/graphql/templates/copilot-secret.yaml b/.github/helm/affine/charts/graphql/templates/copilot-secret.yaml index 5e08f72619..1199fcdb2f 100644 --- a/.github/helm/affine/charts/graphql/templates/copilot-secret.yaml +++ b/.github/helm/affine/charts/graphql/templates/copilot-secret.yaml @@ -9,4 +9,6 @@ data: falSecret: {{ .Values.app.copilot.fal.key | b64enc }} perplexitySecret: {{ .Values.app.copilot.perplexity.key | b64enc }} unsplashSecret: {{ .Values.app.copilot.unsplash.key | b64enc }} + slackBotToken: {{ .Values.app.copilot.slack.botToken | b64enc }} + slackChannelId: {{ .Values.app.copilot.slack.channelId | b64enc }} {{- end }} diff --git a/.github/helm/affine/charts/graphql/templates/copilot-test.yaml b/.github/helm/affine/charts/graphql/templates/copilot-test.yaml new file mode 100644 index 0000000000..64fa3c1754 --- /dev/null +++ b/.github/helm/affine/charts/graphql/templates/copilot-test.yaml @@ -0,0 +1,66 @@ +{{ if .Values.app.copilot.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "graphql.fullname" . }}-copilot-test + labels: + {{- include "graphql.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "1" + "helm.sh/hook-delete-policy": before-hook-creation +spec: + schedule: "0 8 * * *" + jobTemplate: + spec: + template: + spec: + serviceAccountName: {{ include "graphql.serviceAccountName" . }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + command: ["yarn", "test:copilot:e2e:cron"] + env: + - name: AFFINE_ENV + value: "{{ .Release.Namespace }}" + - name: SLACK_BOT_TOKEN + valueFrom: + secretKeyRef: + name: "{{ .Values.app.copilot.secretName }}" + key: slackBotToken + - name: CHANNEL_ID + valueFrom: + secretKeyRef: + name: "{{ .Values.app.copilot.secretName }}" + key: slackChannelId + - name: COPILOT_E2E_ENDPOINT + value: "http://{{ include "graphql.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:3000" + - name: DATABASE_PASSWORD + valueFrom: + 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/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 95b3067851..28686dd2c5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -100,6 +100,9 @@ jobs: COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }} COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }} COPILOT_UNSPLASH_API_KEY: ${{ secrets.COPILOT_UNSPLASH_API_KEY }} + # used for slack notifications + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + RELEASE_SLACK_CHNNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }} METRICS_CUSTOMER_IO_TOKEN: ${{ secrets.METRICS_CUSTOMER_IO_TOKEN }} MAILER_SENDER: ${{ secrets.OAUTH_EMAIL_SENDER }} MAILER_USER: ${{ secrets.OAUTH_EMAIL_LOGIN }} diff --git a/oxlint.json b/oxlint.json index e60ab934fd..27117edfc3 100644 --- a/oxlint.json +++ b/oxlint.json @@ -29,6 +29,7 @@ "**/*.d.ts", "tools/cli/src/webpack/error-handler.js", "packages/backend/native/index.d.ts", + "packages/backend/server/src/__tests__/__snapshots__", "packages/frontend/native/index.d.ts", "packages/frontend/native/index.js", "packages/frontend/graphql/src/graphql/index.ts", diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index c5a32e00c9..ed9907d694 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -17,6 +17,7 @@ "test:coverage": "c8 ava --concurrency 1 --serial", "test:copilot:e2e:coverage": "c8 ava --timeout=5m \"src/__tests__/**/copilot-*.e2e.ts\"", "test:copilot:spec:coverage": "c8 ava --timeout=5m \"src/__tests__/**/copilot-*.spec.ts\"", + "test:copilot:e2e:cron": "node ./scripts/copilot-cron-test.js", "data-migration": "cross-env NODE_ENV=script r ./src/data/index.ts", "predeploy": "yarn prisma migrate deploy && NODE_ENV=script node --import ./scripts/register.js ./dist/data/index.js run", "postinstall": "prisma generate" @@ -61,9 +62,11 @@ "@opentelemetry/semantic-conventions": "^1.28.0", "@prisma/client": "^5.22.0", "@prisma/instrumentation": "^5.22.0", - "@react-email/components": "0.0.31", + "@react-email/components": "0.0.32", "@react-email/render": "1.0.3", + "@slack/web-api": "^7.3.4", "@socket.io/redis-adapter": "^8.3.0", + "ava": "^6.1.2", "cookie-parser": "^1.4.7", "dotenv": "^16.4.7", "eventsource-parser": "^3.0.0", @@ -76,8 +79,10 @@ "html-validate": "^9.0.0", "ioredis": "^5.4.1", "is-mobile": "^5.0.0", + "jsx-slack": "^6.1.1", "keyv": "^5.2.2", "lodash-es": "^4.17.21", + "marked": "^15.0.0", "mixpanel": "^0.18.0", "mustache": "^4.2.0", "nanoid": "^5.0.9", @@ -95,6 +100,8 @@ "ses": "^1.10.0", "socket.io": "^4.8.1", "stripe": "^17.4.0", + "supertest": "^7.0.0", + "tap-parser": "^18.0.0", "ts-node": "^10.9.2", "typescript": "^5.7.2", "winston": "^3.17.0", @@ -120,7 +127,6 @@ "@types/react-dom": "^19.0.2", "@types/sinon": "^17.0.3", "@types/supertest": "^6.0.2", - "ava": "^6.2.0", "c8": "^10.1.3", "cross-env": "^7.0.3", "nodemon": "^3.1.7", diff --git a/packages/backend/server/scripts/copilot-cron-test.js b/packages/backend/server/scripts/copilot-cron-test.js new file mode 100644 index 0000000000..915b68bd79 --- /dev/null +++ b/packages/backend/server/scripts/copilot-cron-test.js @@ -0,0 +1,99 @@ +// start process + +import { spawn } from 'node:child_process'; + +import { WebClient } from '@slack/web-api'; +import { jsxslack } from 'jsx-slack'; +import { marked, Renderer } from 'marked'; +import { Parser } from 'tap-parser'; + +async function runTest() { + const tester = new Promise(resolve => { + const test = spawn( + 'npx', + [ + 'ava', + '--config', + 'tests/ava.docker.config.js', + 'tests/**/copilot-*.e2e.ts', + '--tap', + ], + { env: { ...process.env, NODE_NO_WARNINGS: 1 } } + ); + + const parser = new Parser(); + test.stdout.on('data', data => { + console.log(data.toString()); + parser.write(data); + }); + + test.on('close', _ => { + const failures = parser?.failures.filter(f => !!f.fullname); + const timeouts = parser?.failures.filter(f => !f.fullname); + const result = [ + `${parser.results.pass} passed`, + `${parser.results.fail - timeouts.length} failed`, + `${timeouts.length} timeouts`, + `${parser.results.skip} skipped`, + ]; + const report = [ + `Test finished with ${result.join(', ')}.`, + failures?.length > 0 + ? `Failed tests: \n\n${failures.map(failure => `- ${failure.fullname}`).join('\n')}` + : '', + ]; + resolve(report.join('\n\n')); + }); + }); + + try { + return await tester; + } catch (e) { + return e.message; + } +} + +function render(markdown) { + const rendered = marked(markdown, { + renderer: new (class CustomRenderer extends Renderer { + heading({ tokens }) { + return ` + +
${tokens[0].text}
+ +
`; + } + + paragraph({ tokens }) { + return `

${tokens[0].text}

`; + } + + list(token) { + return `
${super.list(token)}
`; + } + + hr() { + return ``; + } + })(), + }); + return jsxslack([`${rendered}`]); +} + +async function main() { + const { CHANNEL_ID, SLACK_BOT_TOKEN, AFFINE_ENV } = process.env; + + const report = await runTest(); + const blocks = render( + [`# AFFiNE Copilot Test ${AFFINE_ENV} Env Test Result`, report].join('\n\n') + ); + const { ok } = await new WebClient(SLACK_BOT_TOKEN).chat.postMessage({ + channel: CHANNEL_ID, + text: `AFFiNE Copilot Test ${AFFINE_ENV} Env Test Result`, + blocks, + }); + + console.assert(ok, 'Failed to send a message to Slack'); +} + +await main(); diff --git a/packages/backend/server/src/__tests__/ava.docker.config.js b/packages/backend/server/src/__tests__/ava.docker.config.js new file mode 100644 index 0000000000..832ff81f3b --- /dev/null +++ b/packages/backend/server/src/__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/src/__tests__/copilot-provider.e2e.ts b/packages/backend/server/src/__tests__/copilot-provider.e2e.ts index bbf479cda4..989cc2c5b6 100644 --- a/packages/backend/server/src/__tests__/copilot-provider.e2e.ts +++ b/packages/backend/server/src/__tests__/copilot-provider.e2e.ts @@ -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 ( +const runPrisma = async ( cb: ( prisma: InstanceType< typeof import('../../node_modules/@prisma/client').PrismaClient @@ -64,14 +65,99 @@ export const runPrisma = async ( } }; +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); diff --git a/packages/backend/server/src/__tests__/tsconfig.docker.json b/packages/backend/server/src/__tests__/tsconfig.docker.json new file mode 100644 index 0000000000..92b394e7bf --- /dev/null +++ b/packages/backend/server/src/__tests__/tsconfig.docker.json @@ -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" + } + } +} diff --git a/packages/backend/server/src/__tests__/utils/blobs.ts b/packages/backend/server/src/__tests__/utils/blobs.ts index f6832c41c4..845ade0fef 100644 --- a/packages/backend/server/src/__tests__/utils/blobs.ts +++ b/packages/backend/server/src/__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: ` @@ -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 { + 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 { 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/src/__tests__/utils/common.ts b/packages/backend/server/src/__tests__/utils/common.ts index 7729223676..d8e27f72cc 100644 --- a/packages/backend/server/src/__tests__/utils/common.ts +++ b/packages/backend/server/src/__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 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)); +} diff --git a/packages/backend/server/src/__tests__/utils/copilot.ts b/packages/backend/server/src/__tests__/utils/copilot.ts index d4cda549cb..d618705279 100644 --- a/packages/backend/server/src/__tests__/utils/copilot.ts +++ b/packages/backend/server/src/__tests__/utils/copilot.ts @@ -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 { 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 { 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 { 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 ): 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({ @@ -403,7 +402,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/src/__tests__/utils/index.ts b/packages/backend/server/src/__tests__/utils/index.ts index 92904d3c35..1761fa4963 100644 --- a/packages/backend/server/src/__tests__/utils/index.ts +++ b/packages/backend/server/src/__tests__/utils/index.ts @@ -3,3 +3,4 @@ export * from './invite'; export * from './user'; export * from './utils'; export * from './workspace'; +export * from './common'; diff --git a/packages/backend/server/src/__tests__/utils/invite.ts b/packages/backend/server/src/__tests__/utils/invite.ts index 83b0adfacb..fb1414e636 100644 --- a/packages/backend/server/src/__tests__/utils/invite.ts +++ b/packages/backend/server/src/__tests__/utils/invite.ts @@ -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 { 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> { 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 { 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 { 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 { 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 { 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 { 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 { 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/src/__tests__/utils/user.ts b/packages/backend/server/src/__tests__/utils/user.ts index 0925a0658f..e2e27f873b 100644 --- a/packages/backend/server/src/__tests__/utils/user.ts +++ b/packages/backend/server/src/__tests__/utils/user.ts @@ -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 { 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 { 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 { 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 { 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 { 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/src/__tests__/utils/utils.ts b/packages/backend/server/src/__tests__/utils/utils.ts index bb8918f0c4..244751b6ca 100644 --- a/packages/backend/server/src/__tests__/utils/utils.ts +++ b/packages/backend/server/src/__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 '../../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)); -} diff --git a/packages/backend/server/src/__tests__/utils/workspace.ts b/packages/backend/server/src/__tests__/utils/workspace.ts index 0e5ae9acb8..fc1f24f6db 100644 --- a/packages/backend/server/src/__tests__/utils/workspace.ts +++ b/packages/backend/server/src/__tests__/utils/workspace.ts @@ -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 { 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 { 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 { 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({ diff --git a/yarn.lock b/yarn.lock index 7ec9924902..12d90d73da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -805,8 +805,9 @@ __metadata: "@opentelemetry/semantic-conventions": "npm:^1.28.0" "@prisma/client": "npm:^5.22.0" "@prisma/instrumentation": "npm:^5.22.0" - "@react-email/components": "npm:0.0.31" + "@react-email/components": "npm:0.0.32" "@react-email/render": "npm:1.0.3" + "@slack/web-api": "npm:^7.3.4" "@socket.io/redis-adapter": "npm:^8.3.0" "@types/cookie-parser": "npm:^1.4.8" "@types/express": "npm:^4.17.21" @@ -821,7 +822,7 @@ __metadata: "@types/react-dom": "npm:^19.0.2" "@types/sinon": "npm:^17.0.3" "@types/supertest": "npm:^6.0.2" - ava: "npm:^6.2.0" + ava: "npm:^6.1.2" c8: "npm:^10.1.3" cookie-parser: "npm:^1.4.7" cross-env: "npm:^7.0.3" @@ -836,8 +837,10 @@ __metadata: html-validate: "npm:^9.0.0" ioredis: "npm:^5.4.1" is-mobile: "npm:^5.0.0" + jsx-slack: "npm:^6.1.1" keyv: "npm:^5.2.2" lodash-es: "npm:^4.17.21" + marked: "npm:^15.0.0" mixpanel: "npm:^0.18.0" mustache: "npm:^4.2.0" nanoid: "npm:^5.0.9" @@ -860,6 +863,7 @@ __metadata: socket.io: "npm:^4.8.1" stripe: "npm:^17.4.0" supertest: "npm:^7.0.0" + tap-parser: "npm:^18.0.0" ts-node: "npm:^10.9.2" typescript: "npm:^5.7.2" winston: "npm:^3.17.0" @@ -12245,9 +12249,9 @@ __metadata: languageName: node linkType: hard -"@react-email/components@npm:0.0.31": - version: 0.0.31 - resolution: "@react-email/components@npm:0.0.31" +"@react-email/components@npm:0.0.32": + version: 0.0.32 + resolution: "@react-email/components@npm:0.0.32" dependencies: "@react-email/body": "npm:0.0.11" "@react-email/button": "npm:0.0.19" @@ -12264,14 +12268,14 @@ __metadata: "@react-email/link": "npm:0.0.12" "@react-email/markdown": "npm:0.0.14" "@react-email/preview": "npm:0.0.12" - "@react-email/render": "npm:1.0.3" + "@react-email/render": "npm:1.0.4" "@react-email/row": "npm:0.0.12" "@react-email/section": "npm:0.0.16" "@react-email/tailwind": "npm:1.0.4" "@react-email/text": "npm:0.0.11" peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc - checksum: 10/2850943e9e326ea3e9d6a2e55665c5ab6a00f962ff5f01541cca61abcf45770a4cc6a59de503a60eb4cff257805406470ad8a3969b2b5994754bfe1872830d14 + checksum: 10/0a57d122a71581e36f352479165487299d45585656faea5bc66db27637a152ff0bc1edd97da3d78a131d799a3cba188ad7ec85b54987c53c7b24747fdca48bfe languageName: node linkType: hard @@ -12381,6 +12385,20 @@ __metadata: languageName: node linkType: hard +"@react-email/render@npm:1.0.4": + version: 1.0.4 + resolution: "@react-email/render@npm:1.0.4" + dependencies: + html-to-text: "npm:9.0.5" + prettier: "npm:3.4.2" + react-promise-suspense: "npm:0.3.4" + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + checksum: 10/468da62f5fefb75eef813406be1b7f5c73bb60c135c8afb1d0adf511d5b31f807d19821396c2df75a1884998a7ea71b10d99439a2c9b4872adfa273ae70b3af7 + languageName: node + linkType: hard + "@react-email/row@npm:0.0.12": version: 0.0.12 resolution: "@react-email/row@npm:0.0.12" @@ -13176,7 +13194,7 @@ __metadata: languageName: node linkType: hard -"@slack/web-api@npm:^7.8.0": +"@slack/web-api@npm:^7.3.4, @slack/web-api@npm:^7.8.0": version: 7.8.0 resolution: "@slack/web-api@npm:7.8.0" dependencies: @@ -17226,7 +17244,7 @@ __metadata: languageName: node linkType: hard -"ava@npm:^6.2.0": +"ava@npm:^6.1.2, ava@npm:^6.2.0": version: 6.2.0 resolution: "ava@npm:6.2.0" dependencies: @@ -21794,6 +21812,13 @@ __metadata: languageName: node linkType: hard +"events-to-array@npm:^2.0.3": + version: 2.0.3 + resolution: "events-to-array@npm:2.0.3" + checksum: 10/d392eb0013013c3dfa66710a017902760edb2a588f6b1a3f1c92219563ba1c24bcb99c48e3754423a3538ebfd70318c3536d30bfd80c00e7fec77fdd088540d0 + languageName: node + linkType: hard + "events@npm:^3.2.0": version: 3.3.0 resolution: "events@npm:3.3.0" @@ -25498,7 +25523,7 @@ __metadata: languageName: node linkType: hard -"jsx-slack@npm:^6.1.2": +"jsx-slack@npm:^6.1.1, jsx-slack@npm:^6.1.2": version: 6.1.2 resolution: "jsx-slack@npm:6.1.2" dependencies: @@ -26511,7 +26536,7 @@ __metadata: languageName: node linkType: hard -"marked@npm:^15.0.3": +"marked@npm:^15.0.0, marked@npm:^15.0.3": version: 15.0.6 resolution: "marked@npm:15.0.6" bin: @@ -30047,7 +30072,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.2.5, prettier@npm:^3.4.2": +"prettier@npm:3.4.2, prettier@npm:^3.2.5, prettier@npm:^3.4.2": version: 3.4.2 resolution: "prettier@npm:3.4.2" bin: @@ -33537,6 +33562,28 @@ __metadata: languageName: node linkType: hard +"tap-parser@npm:^18.0.0": + version: 18.0.0 + resolution: "tap-parser@npm:18.0.0" + dependencies: + events-to-array: "npm:^2.0.3" + tap-yaml: "npm:4.0.0" + bin: + tap-parser: bin/cmd.cjs + checksum: 10/6e13dc475bfdc880307cc935b9917d43255f7e2b0902d171b7f51b41b029aae9dce5b8683aa48170bb720819b3551d793663c0a11f0660ef78d4a0fe87228ad0 + languageName: node + linkType: hard + +"tap-yaml@npm:4.0.0": + version: 4.0.0 + resolution: "tap-yaml@npm:4.0.0" + dependencies: + yaml: "npm:^2.4.1" + yaml-types: "npm:^0.4.0" + checksum: 10/21d3a27328aa419bb90249357689446488351d350a4ba56c5b1afce9dfe33f60db49014170ea7d16eb90381d3f9f31b59d9637bda632e55167f33c834eb02171 + languageName: node + linkType: hard + "tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": version: 2.2.1 resolution: "tapable@npm:2.2.1" @@ -35971,6 +36018,15 @@ __metadata: languageName: node linkType: hard +"yaml-types@npm:^0.4.0": + version: 0.4.0 + resolution: "yaml-types@npm:0.4.0" + peerDependencies: + yaml: ^2.3.0 + checksum: 10/8a3cd3a0420d5d09981e3e1add46d7482336531e3bdc02192d26caa915c7d0795ad28dd8766e357234d6bfa3a2bd986687f967079e47aecfd4b191250f041cec + languageName: node + linkType: hard + "yaml@npm:^1.10.0": version: 1.10.2 resolution: "yaml@npm:1.10.2" @@ -35978,7 +36034,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.3.1, yaml@npm:^2.3.4, yaml@npm:^2.5.1, yaml@npm:^2.6.1": +"yaml@npm:^2.3.1, yaml@npm:^2.3.4, yaml@npm:^2.4.1, yaml@npm:^2.5.1, yaml@npm:^2.6.1": version: 2.7.0 resolution: "yaml@npm:2.7.0" bin: