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 `
+
+
+
+ `;
+ }
+
+ paragraph({ tokens }) {
+ return ``;
+ }
+
+ list(token) {
+ return ``;
+ }
+
+ 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: