@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.
- package/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +4 -2
- package/src/app/trpc/{[trpc] → edge/[trpc]}/route.ts +3 -3
- package/src/config/__tests__/server.test.ts +0 -11
- package/src/config/file.ts +34 -0
- package/src/config/server/app.ts +0 -8
- package/src/config/server/provider.ts +3 -3
- package/src/database/client/models/file.ts +2 -1
- package/src/database/client/schemas/files.ts +2 -2
- package/src/libs/agent-runtime/google/index.test.ts +20 -1
- package/src/libs/agent-runtime/google/index.ts +22 -9
- package/src/libs/agent-runtime/utils/uriParser.test.ts +29 -0
- package/src/libs/agent-runtime/utils/uriParser.ts +17 -9
- package/src/libs/trpc/client.ts +5 -3
- package/src/libs/trpc/index.ts +10 -34
- package/src/libs/trpc/init.ts +26 -0
- package/src/libs/trpc/middleware/password.test.ts +87 -0
- package/src/libs/trpc/middleware/password.ts +26 -0
- package/src/libs/trpc/middleware/userAuth.test.ts +44 -0
- package/src/libs/trpc/middleware/userAuth.ts +18 -0
- package/src/server/context.ts +28 -3
- package/src/server/files/s3.ts +58 -0
- package/src/server/globalConfig/index.ts +2 -0
- package/src/server/mock.ts +2 -2
- package/src/server/routers/{config → edge/config}/index.test.ts +1 -0
- package/src/server/routers/edge/upload.ts +16 -0
- package/src/server/routers/index.ts +5 -3
- package/src/services/__tests__/global.test.ts +4 -5
- package/src/services/__tests__/sync.test.ts +56 -0
- package/src/services/__tests__/upload.test.ts +72 -0
- package/src/services/_url.ts +2 -0
- package/src/services/file/client.test.ts +102 -34
- package/src/services/file/client.ts +24 -49
- package/src/services/file/type.ts +1 -2
- package/src/services/global.ts +3 -18
- package/src/services/sync.ts +19 -0
- package/src/services/upload.ts +99 -0
- package/src/store/chat/slices/builtinTool/action.test.ts +4 -2
- package/src/store/chat/slices/builtinTool/action.ts +6 -3
- package/src/store/file/slices/images/action.test.ts +10 -17
- package/src/store/file/slices/images/action.ts +4 -1
- package/src/store/file/slices/tts/action.test.ts +8 -14
- package/src/store/file/slices/tts/action.ts +4 -1
- package/src/store/serverConfig/selectors.ts +1 -0
- package/src/store/serverConfig/store.ts +10 -0
- package/src/store/user/slices/common/action.ts +26 -14
- package/src/store/user/slices/sync/action.test.ts +6 -6
- package/src/store/user/slices/sync/action.ts +3 -3
- package/src/types/serverConfig.ts +1 -0
- package/src/app/api/files/image/imgur.ts +0 -72
- package/src/app/api/files/image/route.ts +0 -42
- /package/src/server/routers/{config → edge/config}/__snapshots__/index.test.ts.snap +0 -0
- /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
|
+
});
|
package/src/server/context.ts
CHANGED
|
@@ -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 (
|
|
13
|
-
|
|
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
|
-
|
|
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: {
|
package/src/server/mock.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { createCallerFactory } from '@/libs/trpc';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { edgeRouter } from './routers';
|
|
7
7
|
|
|
8
|
-
export const createCaller = createCallerFactory(
|
|
8
|
+
export const createCaller = createCallerFactory(edgeRouter);
|
|
@@ -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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
+
});
|
package/src/services/_url.ts
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
// Reset all mocks before each test
|
|
42
|
+
vi.resetAllMocks();
|
|
43
|
+
s3Domain = '';
|
|
44
|
+
});
|
|
28
45
|
|
|
29
|
-
|
|
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.
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
(
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
153
|
+
const getFilePromise = fileService.getFile(fileId);
|
|
87
154
|
|
|
88
|
-
|
|
155
|
+
await expect(getFilePromise).rejects.toThrow('file not found');
|
|
156
|
+
});
|
|
89
157
|
});
|
|
90
158
|
});
|