Skip to content

Commit

Permalink
♻️ refactor: refactor global and share service
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx authored and canisminor1990 committed Dec 15, 2023
1 parent 820c15e commit dd6f00e
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 43 deletions.
57 changes: 57 additions & 0 deletions src/services/__tests__/global.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';

import { globalService } from '../global';

global.fetch = vi.fn();

beforeEach(() => {
vi.clearAllMocks();
});

describe('GlobalService', () => {
it('should return the latest version when fetch is successful', async () => {
// Arrange
const mockVersion = '1.0.0';
(fetch as Mock).mockResolvedValue({
json: () => Promise.resolve({ 'dist-tags': { latest: mockVersion } }),
});

// Act
const version = await globalService.getLatestVersion();

// Assert
expect(fetch).toHaveBeenCalledWith('https://registry.npmmirror.com/@lobehub/chat');
expect(version).toBe(mockVersion);
});

it('should return undefined if the latest version is not found in the response', async () => {
// Arrange
(fetch as Mock).mockResolvedValue({
json: () => Promise.resolve({}),
});

// Act
const version = await globalService.getLatestVersion();

// Assert
expect(version).toBeUndefined();
});

it('should throw an error when the fetch call fails', async () => {
// Arrange
(fetch as Mock).mockRejectedValue(new Error('Network error'));

// Act & Assert
await expect(globalService.getLatestVersion()).rejects.toThrow('Network error');
});

it('should handle non-JSON responses gracefully', async () => {
// Arrange
(fetch as Mock).mockResolvedValue({
json: () => Promise.reject(new SyntaxError('Unexpected token < in JSON at position 0')),
});

// Act & Assert
await expect(globalService.getLatestVersion()).rejects.toThrow(SyntaxError);
});
});
86 changes: 86 additions & 0 deletions src/services/__tests__/share.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { ShareGPTConversation } from '@/types/share';
import { parseMarkdown } from '@/utils/parseMarkdown';

import { SHARE_GPT_URL, shareGPTService } from '../share';

// Mock dependencies
vi.mock('@/utils/parseMarkdown', () => ({
parseMarkdown: vi.fn(),
}));

global.fetch = vi.fn();

describe('ShareGPTService', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should create and return a ShareGPT URL when fetch is successful', async () => {
// Arrange
const mockId = '123abc';
const conversation: ShareGPTConversation = {
items: [
{ from: 'human', value: 'Hello' },
{ from: 'gpt', value: 'Hi there!' },
],
};
(parseMarkdown as Mock).mockResolvedValue('Parsed markdown');
(fetch as Mock).mockResolvedValue({
json: () => Promise.resolve({ id: mockId }),
});

// Act
const url = await shareGPTService.createShareGPTUrl(conversation);

// Assert
expect(parseMarkdown).toHaveBeenCalledWith('Hi there!');
expect(fetch).toHaveBeenCalledWith(SHARE_GPT_URL, expect.anything());
expect(url).toBe(`https://shareg.pt/${mockId}`);
});

it('should throw an error when the fetch call fails', async () => {
// Arrange
const conversation: ShareGPTConversation = {
items: [{ from: 'human', value: 'Hello' }],
};
(fetch as Mock).mockRejectedValue(new Error('Network error'));

// Act & Assert
await expect(shareGPTService.createShareGPTUrl(conversation)).rejects.toThrow('Network error');
});

it('should not parse markdown for items not from gpt', async () => {
// Arrange
const mockId = '123abc';
const conversation: ShareGPTConversation = {
items: [
{ from: 'human', value: 'Hello' },
{ from: 'human', value: 'How are you?' },
],
};
(fetch as Mock).mockResolvedValue({
json: () => Promise.resolve({ id: mockId }),
});

// Act
await shareGPTService.createShareGPTUrl(conversation);

// Assert
expect(parseMarkdown).not.toHaveBeenCalled();
});

it('should throw an error if the response does not contain an id', async () => {
// Arrange
const conversation: ShareGPTConversation = {
items: [{ from: 'human', value: 'Hello' }],
};
(fetch as Mock).mockResolvedValue({
json: () => Promise.resolve({}),
});

// Act & Assert
await expect(shareGPTService.createShareGPTUrl(conversation)).rejects.toThrow();
});
});
15 changes: 15 additions & 0 deletions src/services/global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const VERSION_URL = 'https://registry.npmmirror.com/@lobehub/chat';

class GlobalService {
/**
* get latest version from npm
*/
getLatestVersion = async (): Promise<string> => {
const res = await fetch(VERSION_URL);
const data = await res.json();

return data['dist-tags']?.latest;
};
}

export const globalService = new GlobalService();
11 changes: 0 additions & 11 deletions src/services/latestVersion.ts

This file was deleted.

34 changes: 34 additions & 0 deletions src/services/share.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ShareGPTConversation } from '@/types/share';
import { parseMarkdown } from '@/utils/parseMarkdown';

export const SHARE_GPT_URL = 'https://sharegpt.com/api/conversations';

class ShareGPTService {
public async createShareGPTUrl(conversation: ShareGPTConversation) {
const items = [];

for (const item of conversation.items) {
items.push({
from: item.from,
value: item.from === 'gpt' ? await parseMarkdown(item.value) : item.value,
});
}

const res = await fetch(SHARE_GPT_URL, {
body: JSON.stringify({ ...conversation, items }),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
});

const { id } = await res.json();

if (!id) throw new Error('Failed to create ShareGPT URL');

// short link to the ShareGPT post
return `https://shareg.pt/${id}`;
}
}

export const shareGPTService = new ShareGPTService();
28 changes: 0 additions & 28 deletions src/services/shareGPT.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/store/chat/actions/share.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { produce } from 'immer';
import { StateCreator } from 'zustand/vanilla';

import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
import { genShareGPTUrl } from '@/services/shareGPT';
import { shareGPTService } from '@/services/share';
import { useSessionStore } from '@/store/session';
import { agentSelectors } from '@/store/session/selectors';
import { ShareGPTConversation } from '@/types/share';
Expand Down Expand Up @@ -102,7 +102,7 @@ export const chatShare: StateCreator<ChatStore, [['zustand/devtools', never]], [

set({ shareLoading: true });

const res = await genShareGPTUrl({
const res = await shareGPTService.createShareGPTUrl({
avatarUrl: avatar || DEFAULT_USER_AVATAR_URL,
items: shareMsgs,
});
Expand Down
4 changes: 2 additions & 2 deletions src/store/global/slices/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import useSWR, { SWRResponse } from 'swr';
import type { StateCreator } from 'zustand/vanilla';

import { CURRENT_VERSION } from '@/const/version';
import { featLatestVersion } from '@/services/latestVersion';
import { globalService } from '@/services/global';
import { merge } from '@/utils/merge';
import { setNamespace } from '@/utils/storeDebug';

Expand Down Expand Up @@ -72,7 +72,7 @@ export const createCommonSlice: StateCreator<
);
},
useCheckLatestVersion: () =>
useSWR('checkLatestVersion', featLatestVersion, {
useSWR('checkLatestVersion', globalService.getLatestVersion, {
onSuccess: (data: string) => {
if (gt(data, CURRENT_VERSION))
set({ hasNewVersion: true, latestVersion: data }, false, n('checkLatestVersion'));
Expand Down

0 comments on commit dd6f00e

Please sign in to comment.