@lobehub/chat 0.159.12 → 0.160.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/package.json +4 -2
  5. package/src/app/trpc/{[trpc] → edge/[trpc]}/route.ts +3 -3
  6. package/src/config/__tests__/server.test.ts +0 -11
  7. package/src/config/file.ts +34 -0
  8. package/src/config/server/app.ts +0 -8
  9. package/src/config/server/provider.ts +3 -3
  10. package/src/database/client/models/file.ts +2 -1
  11. package/src/database/client/schemas/files.ts +2 -2
  12. package/src/libs/agent-runtime/google/index.test.ts +20 -1
  13. package/src/libs/agent-runtime/google/index.ts +22 -9
  14. package/src/libs/agent-runtime/utils/uriParser.test.ts +29 -0
  15. package/src/libs/agent-runtime/utils/uriParser.ts +17 -9
  16. package/src/libs/trpc/client.ts +5 -3
  17. package/src/libs/trpc/index.ts +10 -34
  18. package/src/libs/trpc/init.ts +26 -0
  19. package/src/libs/trpc/middleware/password.test.ts +87 -0
  20. package/src/libs/trpc/middleware/password.ts +26 -0
  21. package/src/libs/trpc/middleware/userAuth.test.ts +44 -0
  22. package/src/libs/trpc/middleware/userAuth.ts +18 -0
  23. package/src/server/context.ts +28 -3
  24. package/src/server/files/s3.ts +58 -0
  25. package/src/server/globalConfig/index.ts +2 -0
  26. package/src/server/mock.ts +2 -2
  27. package/src/server/routers/{config → edge/config}/index.test.ts +1 -0
  28. package/src/server/routers/edge/upload.ts +16 -0
  29. package/src/server/routers/index.ts +5 -3
  30. package/src/services/__tests__/global.test.ts +4 -5
  31. package/src/services/__tests__/sync.test.ts +56 -0
  32. package/src/services/__tests__/upload.test.ts +72 -0
  33. package/src/services/_url.ts +2 -0
  34. package/src/services/file/client.test.ts +102 -34
  35. package/src/services/file/client.ts +24 -49
  36. package/src/services/file/type.ts +1 -2
  37. package/src/services/global.ts +3 -18
  38. package/src/services/sync.ts +19 -0
  39. package/src/services/upload.ts +99 -0
  40. package/src/store/chat/slices/builtinTool/action.test.ts +4 -2
  41. package/src/store/chat/slices/builtinTool/action.ts +6 -3
  42. package/src/store/file/slices/images/action.test.ts +10 -17
  43. package/src/store/file/slices/images/action.ts +4 -1
  44. package/src/store/file/slices/tts/action.test.ts +8 -14
  45. package/src/store/file/slices/tts/action.ts +4 -1
  46. package/src/store/serverConfig/selectors.ts +1 -0
  47. package/src/store/serverConfig/store.ts +10 -0
  48. package/src/store/user/slices/common/action.ts +26 -14
  49. package/src/store/user/slices/sync/action.test.ts +6 -6
  50. package/src/store/user/slices/sync/action.ts +3 -3
  51. package/src/types/serverConfig.ts +1 -0
  52. package/src/app/api/files/image/imgur.ts +0 -72
  53. package/src/app/api/files/image/route.ts +0 -42
  54. /package/src/server/routers/{config → edge/config}/__snapshots__/index.test.ts.snap +0 -0
  55. /package/src/server/routers/{config → edge/config}/index.ts +0 -0
@@ -0,0 +1,44 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { createCallerFactory } from '@/libs/trpc';
5
+ import { AuthContext, createContextInner } from '@/server/context';
6
+
7
+ import { trpc } from '../init';
8
+ import { userAuth } from './userAuth';
9
+
10
+ const appRouter = trpc.router({
11
+ protectedQuery: trpc.procedure.use(userAuth).query(async ({ ctx }) => {
12
+ return ctx.userId;
13
+ }),
14
+ });
15
+
16
+ const createCaller = createCallerFactory(appRouter);
17
+ let ctx: AuthContext;
18
+ let router: ReturnType<typeof createCaller>;
19
+
20
+ beforeEach(async () => {
21
+ vi.resetAllMocks();
22
+ });
23
+
24
+ describe('userAuth middleware', () => {
25
+ it('should throw UNAUTHORIZED error if userId is not present in context', async () => {
26
+ ctx = await createContextInner();
27
+ router = createCaller(ctx);
28
+
29
+ try {
30
+ await router.protectedQuery();
31
+ } catch (e) {
32
+ expect(e).toEqual(new TRPCError({ code: 'UNAUTHORIZED' }));
33
+ }
34
+ });
35
+
36
+ it('should call next with userId in context if userId is present', async () => {
37
+ ctx = await createContextInner({ userId: 'user-id' });
38
+ router = createCaller(ctx);
39
+
40
+ const data = await router.protectedQuery();
41
+
42
+ expect(data).toEqual('user-id');
43
+ });
44
+ });
@@ -0,0 +1,18 @@
1
+ import { TRPCError } from '@trpc/server';
2
+
3
+ import { trpc } from '../init';
4
+
5
+ export const userAuth = trpc.middleware(async (opts) => {
6
+ const { ctx } = opts;
7
+ // `ctx.user` is nullable
8
+ if (!ctx.userId) {
9
+ throw new TRPCError({ code: 'UNAUTHORIZED' });
10
+ }
11
+
12
+ return opts.next({
13
+ ctx: {
14
+ // ✅ user value is known to be non-null now
15
+ userId: ctx.userId,
16
+ },
17
+ });
18
+ });
@@ -1,7 +1,15 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { getAuth } from '@clerk/nextjs/server';
2
3
  import { NextRequest } from 'next/server';
3
4
 
5
+ import { JWTPayload, LOBE_CHAT_AUTH_HEADER, enableClerk } from '@/const/auth';
6
+
7
+ type ClerkAuth = ReturnType<typeof getAuth>;
8
+
4
9
  export interface AuthContext {
10
+ auth?: ClerkAuth;
11
+ authorizationHeader?: string | null;
12
+ jwtPayload?: JWTPayload | null;
5
13
  userId?: string | null;
6
14
  }
7
15
 
@@ -9,8 +17,14 @@ export interface AuthContext {
9
17
  * Inner function for `createContext` where we create the context.
10
18
  * This is useful for testing when we don't want to mock Next.js' request/response
11
19
  */
12
- export const createContextInner = async (): Promise<AuthContext> => ({
13
- userId: null,
20
+ export const createContextInner = async (params?: {
21
+ auth?: ClerkAuth;
22
+ authorizationHeader?: string | null;
23
+ userId?: string | null;
24
+ }): Promise<AuthContext> => ({
25
+ auth: params?.auth,
26
+ authorizationHeader: params?.authorizationHeader,
27
+ userId: params?.userId,
14
28
  });
15
29
 
16
30
  export type Context = Awaited<ReturnType<typeof createContextInner>>;
@@ -22,5 +36,16 @@ export type Context = Awaited<ReturnType<typeof createContextInner>>;
22
36
  export const createContext = async (request: NextRequest): Promise<Context> => {
23
37
  // for API-response caching see https://trpc.io/docs/v11/caching
24
38
 
25
- return createContextInner();
39
+ const authorization = request.headers.get(LOBE_CHAT_AUTH_HEADER);
40
+
41
+ let userId;
42
+ let auth;
43
+
44
+ if (enableClerk) {
45
+ auth = getAuth(request);
46
+
47
+ userId = auth.userId;
48
+ }
49
+
50
+ return createContextInner({ auth, authorizationHeader: authorization, userId });
26
51
  };
@@ -0,0 +1,58 @@
1
+ import { ListObjectsCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
2
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
3
+ import { z } from 'zod';
4
+
5
+ import { fileEnv } from '@/config/file';
6
+
7
+ export const fileSchema = z.object({
8
+ Key: z.string(),
9
+ LastModified: z.date(),
10
+ Size: z.number(),
11
+ });
12
+
13
+ export const listFileSchema = z.array(fileSchema);
14
+
15
+ export type FileType = z.infer<typeof fileSchema>;
16
+
17
+ const DEFAULT_S3_REGION = 'us-east-1';
18
+
19
+ export class S3 {
20
+ private readonly client: S3Client;
21
+
22
+ private readonly bucket: string;
23
+
24
+ constructor() {
25
+ if (!fileEnv.S3_ACCESS_KEY_ID || !fileEnv.S3_SECRET_ACCESS_KEY || !fileEnv.S3_BUCKET)
26
+ throw new Error('S3 environment variables are not set completely, please check your env');
27
+
28
+ this.bucket = fileEnv.S3_BUCKET;
29
+
30
+ this.client = new S3Client({
31
+ credentials: {
32
+ accessKeyId: fileEnv.S3_ACCESS_KEY_ID,
33
+ secretAccessKey: fileEnv.S3_SECRET_ACCESS_KEY,
34
+ },
35
+ endpoint: fileEnv.S3_ENDPOINT,
36
+ region: fileEnv.S3_REGION || DEFAULT_S3_REGION,
37
+ });
38
+ }
39
+
40
+ public async getImages(): Promise<FileType[]> {
41
+ const command = new ListObjectsCommand({
42
+ Bucket: this.bucket,
43
+ });
44
+
45
+ const res = await this.client.send(command);
46
+ return listFileSchema.parse(res.Contents);
47
+ }
48
+
49
+ public async createPreSignedUrl(key: string): Promise<string> {
50
+ const command = new PutObjectCommand({
51
+ ACL: 'public-read',
52
+ Bucket: this.bucket,
53
+ Key: key,
54
+ });
55
+
56
+ return getSignedUrl(this.client, command, { expiresIn: 3600 });
57
+ }
58
+ }
@@ -1,3 +1,4 @@
1
+ import { fileEnv } from '@/config/file';
1
2
  import {
2
3
  OllamaProviderCard,
3
4
  OpenAIProviderCard,
@@ -50,6 +51,7 @@ export const getServerGlobalConfig = () => {
50
51
  config: parseAgentConfig(DEFAULT_AGENT_CONFIG),
51
52
  },
52
53
 
54
+ enableUploadFileToServer: !!fileEnv.S3_SECRET_ACCESS_KEY,
53
55
  enabledAccessCode: ACCESS_CODES?.length > 0,
54
56
  enabledOAuthSSO: enableNextAuth,
55
57
  languageModel: {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  import { createCallerFactory } from '@/libs/trpc';
5
5
 
6
- import { appRouter } from './routers';
6
+ import { edgeRouter } from './routers';
7
7
 
8
- export const createCaller = createCallerFactory(appRouter);
8
+ export const createCaller = createCallerFactory(edgeRouter);
@@ -1,3 +1,4 @@
1
+ // @vitest-environment node
1
2
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
3
 
3
4
  /**
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+
3
+ import { passwordProcedure, router } from '@/libs/trpc';
4
+ import { S3 } from '@/server/files/s3';
5
+
6
+ export const uploadRouter = router({
7
+ createS3PreSignedUrl: passwordProcedure
8
+ .input(z.object({ pathname: z.string() }))
9
+ .mutation(async ({ input }) => {
10
+ const s3 = new S3();
11
+
12
+ return await s3.createPreSignedUrl(input.pathname);
13
+ }),
14
+ });
15
+
16
+ export type FileRouter = typeof uploadRouter;
@@ -3,11 +3,13 @@
3
3
  */
4
4
  import { publicProcedure, router } from '@/libs/trpc';
5
5
 
6
- import { configRouter } from './config';
6
+ import { configRouter } from './edge/config';
7
+ import { uploadRouter } from './edge/upload';
7
8
 
8
- export const appRouter = router({
9
+ export const edgeRouter = router({
9
10
  config: configRouter,
10
11
  healthcheck: publicProcedure.query(() => "i'm live!"),
12
+ upload: uploadRouter,
11
13
  });
12
14
 
13
- export type AppRouter = typeof appRouter;
15
+ export type EdgeRouter = typeof edgeRouter;
@@ -1,7 +1,6 @@
1
1
  import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { trpcClient } from '@/libs/trpc/client';
4
- import { LobeAgentConfig } from '@/types/agent';
3
+ import { edgeClient } from '@/libs/trpc/client';
5
4
  import { GlobalServerConfig } from '@/types/serverConfig';
6
5
 
7
6
  import { globalService } from '../global';
@@ -14,7 +13,7 @@ beforeEach(() => {
14
13
 
15
14
  vi.mock('@/libs/trpc/client', () => {
16
15
  return {
17
- trpcClient: {
16
+ edgeClient: {
18
17
  config: {
19
18
  getGlobalConfig: { query: vi.fn() },
20
19
  getDefaultAgentConfig: { query: vi.fn() },
@@ -76,7 +75,7 @@ describe('GlobalService', () => {
76
75
  it('should return the serverConfig when fetch is successful', async () => {
77
76
  // Arrange
78
77
  const mockConfig = { enabledOAuthSSO: true } as GlobalServerConfig;
79
- vi.spyOn(trpcClient.config.getGlobalConfig, 'query').mockResolvedValue(mockConfig);
78
+ vi.spyOn(edgeClient.config.getGlobalConfig, 'query').mockResolvedValue(mockConfig);
80
79
 
81
80
  // Act
82
81
  const config = await globalService.getGlobalConfig();
@@ -87,7 +86,7 @@ describe('GlobalService', () => {
87
86
 
88
87
  it('should return the defaultAgentConfig when fetch is successful', async () => {
89
88
  // Arrange
90
- vi.spyOn(trpcClient.config.getDefaultAgentConfig, 'query').mockResolvedValue({
89
+ vi.spyOn(edgeClient.config.getDefaultAgentConfig, 'query').mockResolvedValue({
91
90
  model: 'gemini-pro',
92
91
  });
93
92
 
@@ -0,0 +1,56 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { dataSync } from '@/database/client/core';
4
+ import { StartDataSyncParams } from '@/types/sync';
5
+
6
+ import { syncService } from '../sync';
7
+
8
+ vi.mock('@/database/client/core', () => ({
9
+ dataSync: {
10
+ startDataSync: vi.fn(),
11
+ disconnect: vi.fn(),
12
+ },
13
+ }));
14
+
15
+ describe('SyncService', () => {
16
+ afterEach(() => {
17
+ vi.resetAllMocks();
18
+ });
19
+
20
+ describe('enabledSync', () => {
21
+ it('should return false when running on server side', async () => {
22
+ const params = { user: { id: '123' }, authToken: 'abc' } as unknown as StartDataSyncParams;
23
+
24
+ const origin = global.window;
25
+
26
+ // @ts-ignore
27
+ global.window = undefined;
28
+
29
+ const result = await syncService.enabledSync(params);
30
+
31
+ expect(result).toBe(false);
32
+ expect(dataSync.startDataSync).not.toHaveBeenCalled();
33
+
34
+ // reset
35
+ global.window = origin;
36
+ });
37
+
38
+ it('should start data sync and return true when running on client side', async () => {
39
+ const params = { user: { id: '123' }, authToken: 'abc' } as unknown as StartDataSyncParams;
40
+
41
+ const result = await syncService.enabledSync(params);
42
+
43
+ expect(result).toBe(true);
44
+ expect(dataSync.startDataSync).toHaveBeenCalledWith(params);
45
+ });
46
+ });
47
+
48
+ describe('disableSync', () => {
49
+ it('should disconnect data sync and return false', async () => {
50
+ const result = await syncService.disableSync();
51
+
52
+ expect(result).toBe(false);
53
+ expect(dataSync.disconnect).toHaveBeenCalled();
54
+ });
55
+ });
56
+ });
@@ -0,0 +1,72 @@
1
+ import { beforeAll, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { DB_File } from '@/database/client/schemas/files';
4
+ import { edgeClient } from '@/libs/trpc/client';
5
+ import { API_ENDPOINTS } from '@/services/_url';
6
+ import { serverConfigSelectors } from '@/store/serverConfig/selectors';
7
+ import { createServerConfigStore } from '@/store/serverConfig/store';
8
+
9
+ import { uploadService } from '../upload';
10
+
11
+ vi.mock('@/store/serverConfig/selectors');
12
+ vi.mock('@/libs/trpc/client', () => {
13
+ return {
14
+ edgeClient: {
15
+ upload: {
16
+ createS3PreSignedUrl: { mutate: vi.fn() },
17
+ },
18
+ },
19
+ };
20
+ });
21
+
22
+ beforeAll(() => {
23
+ createServerConfigStore();
24
+ });
25
+
26
+ describe('UploadService', () => {
27
+ describe('uploadFile', () => {
28
+ it('should upload file to server when enableServer is true', async () => {
29
+ // Arrange
30
+ const file: DB_File = {
31
+ data: new ArrayBuffer(10),
32
+ fileType: 'text/plain',
33
+ metadata: {},
34
+ name: 'test.txt',
35
+ saveMode: 'local',
36
+ size: 10,
37
+ };
38
+ const mockCreateS3Url = vi.fn().mockResolvedValue('https://example.com');
39
+ vi.mocked(edgeClient.upload.createS3PreSignedUrl.mutate).mockImplementation(mockCreateS3Url);
40
+ vi.spyOn(serverConfigSelectors, 'enableUploadFileToServer').mockReturnValue(true);
41
+ global.fetch = vi.fn().mockResolvedValue({ ok: true } as Response);
42
+
43
+ // Act
44
+ const result = await uploadService.uploadFile(file);
45
+
46
+ // Assert
47
+ expect(mockCreateS3Url).toHaveBeenCalledTimes(1);
48
+ expect(fetch).toHaveBeenCalledTimes(1);
49
+ expect(result.url).toMatch(/\/\d+\/[a-f0-9-]+\.txt/);
50
+ expect(result.saveMode).toBe('url');
51
+ });
52
+
53
+ it('should save file locally when enableServer is false', async () => {
54
+ // Arrange
55
+ const file: DB_File = {
56
+ data: new ArrayBuffer(10),
57
+ fileType: 'text/plain',
58
+ metadata: {},
59
+ name: 'test.txt',
60
+ saveMode: 'local',
61
+ size: 10,
62
+ };
63
+ vi.spyOn(serverConfigSelectors, 'enableUploadFileToServer').mockReturnValue(false);
64
+
65
+ // Act
66
+ const result = await uploadService.uploadFile(file);
67
+
68
+ // Assert
69
+ expect(result).toEqual(file);
70
+ });
71
+ });
72
+ });
@@ -1,3 +1,5 @@
1
+ // TODO: 未来所有路由需要全部迁移到 trpc
2
+
1
3
  /* eslint-disable sort-keys-fix/sort-keys-fix */
2
4
  import { transform } from 'lodash-es';
3
5
 
@@ -1,18 +1,35 @@
1
- import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
1
+ import { Mock, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
+ import { fileEnv } from '@/config/file';
3
4
  import { FileModel } from '@/database/client/models/file';
4
5
  import { DB_File } from '@/database/client/schemas/files';
6
+ import { serverConfigSelectors } from '@/store/serverConfig/selectors';
7
+ import { createServerConfigStore } from '@/store/serverConfig/store';
5
8
 
6
9
  import { ClientService } from './client';
7
10
 
8
11
  const fileService = new ClientService();
9
12
 
13
+ beforeAll(() => {
14
+ createServerConfigStore();
15
+ });
10
16
  // Mocks for the FileModel
11
17
  vi.mock('@/database/client/models/file', () => ({
12
18
  FileModel: {
13
19
  create: vi.fn(),
14
20
  delete: vi.fn(),
15
21
  findById: vi.fn(),
22
+ clear: vi.fn(),
23
+ },
24
+ }));
25
+
26
+ let s3Domain: string;
27
+
28
+ vi.mock('@/config/file', () => ({
29
+ fileEnv: {
30
+ get NEXT_PUBLIC_S3_DOMAIN() {
31
+ return s3Domain;
32
+ },
16
33
  },
17
34
  }));
18
35
 
@@ -20,13 +37,14 @@ vi.mock('@/database/client/models/file', () => ({
20
37
  global.URL.createObjectURL = vi.fn();
21
38
  global.Blob = vi.fn();
22
39
 
23
- describe('FileService', () => {
24
- beforeEach(() => {
25
- // Reset all mocks before each test
26
- vi.resetAllMocks();
27
- });
40
+ beforeEach(() => {
41
+ // Reset all mocks before each test
42
+ vi.resetAllMocks();
43
+ s3Domain = '';
44
+ });
28
45
 
29
- it('uploadFile should save the file to the database', async () => {
46
+ describe('FileService', () => {
47
+ it('createFile should save the file to the database', async () => {
30
48
  const localFile: DB_File = {
31
49
  name: 'test',
32
50
  data: new ArrayBuffer(1),
@@ -37,7 +55,7 @@ describe('FileService', () => {
37
55
 
38
56
  (FileModel.create as Mock).mockResolvedValue(localFile);
39
57
 
40
- const result = await fileService.uploadFile(localFile);
58
+ const result = await fileService.createFile(localFile);
41
59
 
42
60
  expect(FileModel.create).toHaveBeenCalledWith(localFile);
43
61
  expect(result).toEqual(localFile);
@@ -53,38 +71,88 @@ describe('FileService', () => {
53
71
  expect(result).toBe(true);
54
72
  });
55
73
 
56
- it('getFile should retrieve and convert file info to FilePreview', async () => {
57
- const fileId = '1';
58
- const fileData: DB_File = {
59
- name: 'test',
60
- data: new ArrayBuffer(1),
61
- fileType: 'image/png',
62
- saveMode: 'local',
63
- size: 1,
64
- };
74
+ describe('getFile', () => {
75
+ it('should retrieve and convert local file info to FilePreview', async () => {
76
+ const fileId = '1';
77
+ const fileData: DB_File = {
78
+ name: 'test',
79
+ data: new ArrayBuffer(1),
80
+ fileType: 'image/png',
81
+ saveMode: 'local',
82
+ size: 1,
83
+ };
84
+
85
+ (FileModel.findById as Mock).mockResolvedValue(fileData);
86
+ (global.URL.createObjectURL as Mock).mockReturnValue('blob:test');
87
+ (global.Blob as Mock).mockImplementation(() => ['test']);
88
+
89
+ const result = await fileService.getFile(fileId);
90
+
91
+ expect(FileModel.findById).toHaveBeenCalledWith(fileId);
92
+ expect(result).toEqual({
93
+ base64Url: 'data:image/png;base64,AA==',
94
+ fileType: 'image/png',
95
+ name: 'test',
96
+ saveMode: 'local',
97
+ url: 'blob:test',
98
+ });
99
+ });
65
100
 
66
- (FileModel.findById as Mock).mockResolvedValue(fileData);
67
- (global.URL.createObjectURL as Mock).mockReturnValue('blob:test');
68
- (global.Blob as Mock).mockImplementation(() => ['test']);
101
+ it('should retrieve and convert URL file info to FilePreview when enableServer is true', async () => {
102
+ const fileId = '1';
103
+ const fileData: DB_File = {
104
+ name: 'test',
105
+ fileType: 'image/png',
106
+ saveMode: 'url',
107
+ metadata: {
108
+ filename: 'test.png',
109
+ },
110
+ size: 1,
111
+ url: '/test.png',
112
+ };
113
+
114
+ (FileModel.findById as Mock).mockResolvedValue(fileData);
115
+ vi.spyOn(serverConfigSelectors, 'enableUploadFileToServer').mockReturnValue(true);
116
+ s3Domain = 'https://example.com';
117
+
118
+ const result = await fileService.getFile(fileId);
119
+
120
+ expect(FileModel.findById).toHaveBeenCalledWith(fileId);
121
+ expect(result).toEqual({
122
+ fileType: 'image/png',
123
+ name: 'test.png',
124
+ saveMode: 'url',
125
+ url: 'https://example.com/test.png',
126
+ });
127
+ });
69
128
 
70
- const result = await fileService.getFile(fileId);
129
+ it('should throw an error when enableServer is true but NEXT_PUBLIC_S3_DOMAIN is not set', async () => {
130
+ const fileId = '1';
131
+ const fileData: DB_File = {
132
+ name: 'test',
133
+ fileType: 'image/png',
134
+ saveMode: 'url',
135
+ size: 1,
136
+ url: '/test.png',
137
+ };
71
138
 
72
- expect(FileModel.findById).toHaveBeenCalledWith(fileId);
73
- expect(result).toEqual({
74
- base64Url: 'data:image/png;base64,AA==',
75
- fileType: 'image/png',
76
- name: 'test',
77
- saveMode: 'local',
78
- url: 'blob:test',
139
+ (FileModel.findById as Mock).mockResolvedValue(fileData);
140
+ vi.spyOn(serverConfigSelectors, 'enableUploadFileToServer').mockReturnValue(true);
141
+
142
+ const getFilePromise = fileService.getFile(fileId);
143
+
144
+ await expect(getFilePromise).rejects.toThrow(
145
+ 'fileEnv.NEXT_PUBLIC_S3_DOMAIN is not set while enable server upload',
146
+ );
79
147
  });
80
- });
81
148
 
82
- it('getFile should throw an error when the file is not found', async () => {
83
- const fileId = 'non-existent';
84
- (FileModel.findById as Mock).mockResolvedValue(null);
149
+ it('should throw an error when the file is not found', async () => {
150
+ const fileId = 'non-existent';
151
+ (FileModel.findById as Mock).mockResolvedValue(null);
85
152
 
86
- const getFilePromise = fileService.getFile(fileId);
153
+ const getFilePromise = fileService.getFile(fileId);
87
154
 
88
- await expect(getFilePromise).rejects.toThrow('file not found');
155
+ await expect(getFilePromise).rejects.toThrow('file not found');
156
+ });
89
157
  });
90
158
  });