mirror of
https://github.com/langgenius/dify.git
synced 2026-03-28 05:00:59 -04:00
refactor: use ungh for github api (#34108)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useGitHubReleases, useGitHubUpload } from '../hooks'
|
||||
import { checkForUpdates, fetchReleases, handleUpload } from '../hooks'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
@@ -15,10 +14,6 @@ vi.mock('@/app/components/base/ui/toast', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
GITHUB_ACCESS_TOKEN: '',
|
||||
}))
|
||||
|
||||
const mockUploadGitHub = vi.fn()
|
||||
vi.mock('@/service/plugins', () => ({
|
||||
uploadGitHub: (...args: unknown[]) => mockUploadGitHub(...args),
|
||||
@@ -37,17 +32,17 @@ describe('install-plugin/hooks', () => {
|
||||
it('fetches releases from GitHub API and formats them', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([
|
||||
{
|
||||
tag_name: 'v1.0.0',
|
||||
assets: [{ browser_download_url: 'https://example.com/v1.zip', name: 'plugin.zip' }],
|
||||
body: 'Release notes',
|
||||
},
|
||||
]),
|
||||
json: () => Promise.resolve({
|
||||
releases: [
|
||||
{
|
||||
tag: 'v1.0.0',
|
||||
assets: [{ downloadUrl: 'https://example.com/plugin.zip' }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = await result.current.fetchReleases('owner', 'repo')
|
||||
const releases = await fetchReleases('owner', 'repo')
|
||||
|
||||
expect(releases).toHaveLength(1)
|
||||
expect(releases[0].tag_name).toBe('v1.0.0')
|
||||
@@ -60,8 +55,7 @@ describe('install-plugin/hooks', () => {
|
||||
ok: false,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = await result.current.fetchReleases('owner', 'repo')
|
||||
const releases = await fetchReleases('owner', 'repo')
|
||||
|
||||
expect(releases).toEqual([])
|
||||
expect(mockNotify).toHaveBeenCalledWith('Failed to fetch repository releases')
|
||||
@@ -70,29 +64,26 @@ describe('install-plugin/hooks', () => {
|
||||
|
||||
describe('checkForUpdates', () => {
|
||||
it('detects newer version available', () => {
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = [
|
||||
{ tag_name: 'v1.0.0', assets: [] },
|
||||
{ tag_name: 'v2.0.0', assets: [] },
|
||||
]
|
||||
const { needUpdate, toastProps } = result.current.checkForUpdates(releases, 'v1.0.0')
|
||||
const { needUpdate, toastProps } = checkForUpdates(releases, 'v1.0.0')
|
||||
expect(needUpdate).toBe(true)
|
||||
expect(toastProps.message).toContain('v2.0.0')
|
||||
})
|
||||
|
||||
it('returns no update when current is latest', () => {
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const releases = [
|
||||
{ tag_name: 'v1.0.0', assets: [] },
|
||||
]
|
||||
const { needUpdate, toastProps } = result.current.checkForUpdates(releases, 'v1.0.0')
|
||||
const { needUpdate, toastProps } = checkForUpdates(releases, 'v1.0.0')
|
||||
expect(needUpdate).toBe(false)
|
||||
expect(toastProps.type).toBe('info')
|
||||
})
|
||||
|
||||
it('returns error for empty releases', () => {
|
||||
const { result } = renderHook(() => useGitHubReleases())
|
||||
const { needUpdate, toastProps } = result.current.checkForUpdates([], 'v1.0.0')
|
||||
const { needUpdate, toastProps } = checkForUpdates([], 'v1.0.0')
|
||||
expect(needUpdate).toBe(false)
|
||||
expect(toastProps.type).toBe('error')
|
||||
expect(toastProps.message).toContain('empty')
|
||||
@@ -109,8 +100,7 @@ describe('install-plugin/hooks', () => {
|
||||
})
|
||||
const onSuccess = vi.fn()
|
||||
|
||||
const { result } = renderHook(() => useGitHubUpload())
|
||||
const pkg = await result.current.handleUpload(
|
||||
const pkg = await handleUpload(
|
||||
'https://github.com/owner/repo',
|
||||
'v1.0.0',
|
||||
'plugin.difypkg',
|
||||
@@ -132,9 +122,8 @@ describe('install-plugin/hooks', () => {
|
||||
it('shows toast on upload error', async () => {
|
||||
mockUploadGitHub.mockRejectedValue(new Error('Upload failed'))
|
||||
|
||||
const { result } = renderHook(() => useGitHubUpload())
|
||||
await expect(
|
||||
result.current.handleUpload('url', 'v1', 'pkg'),
|
||||
handleUpload('url', 'v1', 'pkg'),
|
||||
).rejects.toThrow('Upload failed')
|
||||
expect(mockNotify).toHaveBeenCalledWith('Error uploading package')
|
||||
})
|
||||
|
||||
@@ -1,101 +1,87 @@
|
||||
import type { GitHubRepoReleaseResponse } from '../types'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { GITHUB_ACCESS_TOKEN } from '@/config'
|
||||
import { uploadGitHub } from '@/service/plugins'
|
||||
import { compareVersion, getLatestVersion } from '@/utils/semver'
|
||||
|
||||
const normalizeAssetName = (downloadUrl: string) => {
|
||||
const parts = downloadUrl.split('/')
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
|
||||
const formatReleases = (releases: any) => {
|
||||
return releases.map((release: any) => ({
|
||||
tag_name: release.tag_name,
|
||||
tag_name: release.tag,
|
||||
assets: release.assets.map((asset: any) => ({
|
||||
browser_download_url: asset.browser_download_url,
|
||||
name: asset.name,
|
||||
browser_download_url: asset.downloadUrl,
|
||||
name: normalizeAssetName(asset.downloadUrl),
|
||||
})),
|
||||
}))
|
||||
}
|
||||
|
||||
export const useGitHubReleases = () => {
|
||||
const fetchReleases = async (owner: string, repo: string) => {
|
||||
try {
|
||||
if (!GITHUB_ACCESS_TOKEN) {
|
||||
// Fetch releases without authentication from client
|
||||
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`)
|
||||
if (!res.ok)
|
||||
throw new Error('Failed to fetch repository releases')
|
||||
const data = await res.json()
|
||||
return formatReleases(data)
|
||||
}
|
||||
else {
|
||||
// Fetch releases with authentication from server
|
||||
const res = await fetch(`/repos/${owner}/${repo}/releases`)
|
||||
const bodyJson = await res.json()
|
||||
if (bodyJson.status !== 200)
|
||||
throw new Error(bodyJson.data.message)
|
||||
return formatReleases(bodyJson.data)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message)
|
||||
}
|
||||
else {
|
||||
toast.error('Failed to fetch repository releases')
|
||||
}
|
||||
return []
|
||||
}
|
||||
export const fetchReleases = async (owner: string, repo: string) => {
|
||||
try {
|
||||
// Fetch releases without authentication from client
|
||||
const res = await fetch(`https://ungh.cc/repos/${owner}/${repo}/releases`)
|
||||
if (!res.ok)
|
||||
throw new Error('Failed to fetch repository releases')
|
||||
const data = await res.json()
|
||||
return formatReleases(data.releases)
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message)
|
||||
}
|
||||
else {
|
||||
toast.error('Failed to fetch repository releases')
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => {
|
||||
let needUpdate = false
|
||||
const toastProps: { type?: 'success' | 'error' | 'info' | 'warning', message: string } = {
|
||||
type: 'info',
|
||||
message: 'No new version available',
|
||||
}
|
||||
if (fetchedReleases.length === 0) {
|
||||
toastProps.type = 'error'
|
||||
toastProps.message = 'Input releases is empty'
|
||||
return { needUpdate, toastProps }
|
||||
}
|
||||
const versions = fetchedReleases.map(release => release.tag_name)
|
||||
const latestVersion = getLatestVersion(versions)
|
||||
try {
|
||||
needUpdate = compareVersion(latestVersion, currentVersion) === 1
|
||||
if (needUpdate)
|
||||
toastProps.message = `New version available: ${latestVersion}`
|
||||
}
|
||||
catch {
|
||||
needUpdate = false
|
||||
toastProps.type = 'error'
|
||||
toastProps.message = 'Fail to compare versions, please check the version format'
|
||||
}
|
||||
export const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => {
|
||||
let needUpdate = false
|
||||
const toastProps: { type?: 'success' | 'error' | 'info' | 'warning', message: string } = {
|
||||
type: 'info',
|
||||
message: 'No new version available',
|
||||
}
|
||||
if (fetchedReleases.length === 0) {
|
||||
toastProps.type = 'error'
|
||||
toastProps.message = 'Input releases is empty'
|
||||
return { needUpdate, toastProps }
|
||||
}
|
||||
|
||||
return { fetchReleases, checkForUpdates }
|
||||
}
|
||||
|
||||
export const useGitHubUpload = () => {
|
||||
const handleUpload = async (
|
||||
repoUrl: string,
|
||||
selectedVersion: string,
|
||||
selectedPackage: string,
|
||||
onSuccess?: (GitHubPackage: { manifest: any, unique_identifier: string }) => void,
|
||||
) => {
|
||||
try {
|
||||
const response = await uploadGitHub(repoUrl, selectedVersion, selectedPackage)
|
||||
const GitHubPackage = {
|
||||
manifest: response.manifest,
|
||||
unique_identifier: response.unique_identifier,
|
||||
}
|
||||
if (onSuccess)
|
||||
onSuccess(GitHubPackage)
|
||||
return GitHubPackage
|
||||
}
|
||||
catch (error) {
|
||||
toast.error('Error uploading package')
|
||||
throw error
|
||||
}
|
||||
const versions = fetchedReleases.map(release => release.tag_name)
|
||||
const latestVersion = getLatestVersion(versions)
|
||||
try {
|
||||
needUpdate = compareVersion(latestVersion, currentVersion) === 1
|
||||
if (needUpdate)
|
||||
toastProps.message = `New version available: ${latestVersion}`
|
||||
}
|
||||
catch {
|
||||
needUpdate = false
|
||||
toastProps.type = 'error'
|
||||
toastProps.message = 'Fail to compare versions, please check the version format'
|
||||
}
|
||||
return { needUpdate, toastProps }
|
||||
}
|
||||
|
||||
export const handleUpload = async (
|
||||
repoUrl: string,
|
||||
selectedVersion: string,
|
||||
selectedPackage: string,
|
||||
onSuccess?: (GitHubPackage: { manifest: any, unique_identifier: string }) => void,
|
||||
) => {
|
||||
try {
|
||||
const response = await uploadGitHub(repoUrl, selectedVersion, selectedPackage)
|
||||
const GitHubPackage = {
|
||||
manifest: response.manifest,
|
||||
unique_identifier: response.unique_identifier,
|
||||
}
|
||||
if (onSuccess)
|
||||
onSuccess(GitHubPackage)
|
||||
return GitHubPackage
|
||||
}
|
||||
catch (error) {
|
||||
toast.error('Error uploading package')
|
||||
throw error
|
||||
}
|
||||
|
||||
return { handleUpload }
|
||||
}
|
||||
|
||||
@@ -74,10 +74,16 @@ vi.mock('@/app/components/plugins/install-plugin/base/use-get-icon', () => ({
|
||||
default: () => ({ getIconUrl: mockGetIconUrl }),
|
||||
}))
|
||||
|
||||
const mockFetchReleases = vi.fn()
|
||||
vi.mock('../../hooks', () => ({
|
||||
useGitHubReleases: () => ({ fetchReleases: mockFetchReleases }),
|
||||
const { mockFetchReleases } = vi.hoisted(() => ({
|
||||
mockFetchReleases: vi.fn(),
|
||||
}))
|
||||
vi.mock('../../hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../hooks')>()
|
||||
return {
|
||||
...actual,
|
||||
fetchReleases: mockFetchReleases,
|
||||
}
|
||||
})
|
||||
|
||||
const mockRefreshPluginList = vi.fn()
|
||||
vi.mock('../../hooks/use-refresh-plugin-list', () => ({
|
||||
|
||||
@@ -12,7 +12,7 @@ import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-ico
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { InstallStepFromGitHub } from '../../types'
|
||||
import Installed from '../base/installed'
|
||||
import { useGitHubReleases } from '../hooks'
|
||||
import { fetchReleases } from '../hooks'
|
||||
import useHideLogic from '../hooks/use-hide-logic'
|
||||
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
|
||||
import { convertRepoToUrl, parseGitHubUrl } from '../utils'
|
||||
@@ -31,7 +31,6 @@ type InstallFromGitHubProps = {
|
||||
const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, onClose, onSuccess }) => {
|
||||
const { t } = useTranslation()
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const { fetchReleases } = useGitHubReleases()
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
|
||||
const {
|
||||
|
||||
@@ -5,11 +5,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../../../types'
|
||||
import SelectPackage from '../selectPackage'
|
||||
|
||||
// Mock the useGitHubUpload hook
|
||||
const mockHandleUpload = vi.fn()
|
||||
vi.mock('../../../hooks', () => ({
|
||||
useGitHubUpload: () => ({ handleUpload: mockHandleUpload }),
|
||||
// Mock upload helper from hooks module
|
||||
const { mockHandleUpload } = vi.hoisted(() => ({
|
||||
mockHandleUpload: vi.fn(),
|
||||
}))
|
||||
vi.mock('../../../hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../hooks')>()
|
||||
return {
|
||||
...actual,
|
||||
handleUpload: mockHandleUpload,
|
||||
}
|
||||
})
|
||||
|
||||
// Factory functions
|
||||
const createMockManifest = (): PluginDeclaration => ({
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import { useGitHubUpload } from '../../hooks'
|
||||
import { handleUpload } from '../../hooks'
|
||||
|
||||
const i18nPrefix = 'installFromGitHub'
|
||||
|
||||
@@ -43,7 +43,6 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const isEdit = Boolean(updatePayload)
|
||||
const [isUploading, setIsUploading] = React.useState(false)
|
||||
const { handleUpload } = useGitHubUpload()
|
||||
|
||||
const handleUploadPackage = async () => {
|
||||
if (isUploading)
|
||||
|
||||
Reference in New Issue
Block a user