mirror of
https://github.com/langgenius/dify.git
synced 2026-04-23 20:36:14 +08:00
fix: update test files to use TanStack Query pattern for appDetail
Update test files to reflect the appDetail migration from Zustand to TanStack Query: - Replace setAppDetail mocks with useInvalidateAppDetail - Add useParams mock from next/navigation - Add useAppDetail mock from @/service/use-apps - Remove deprecated fetchAppDetail + setAppDetail patterns - Fix marketplace test mock data types
This commit is contained in:
@ -2,11 +2,13 @@ import type { ReactNode } from 'react'
|
||||
import type { IConfigVarProps } from './index'
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import type { App } from '@/types/app'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import DebugConfigurationContext from '@/context/debug-configuration'
|
||||
import { useAppDetail } from '@/service/use-apps'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from './index'
|
||||
@ -38,6 +40,15 @@ vi.mock('@/context/modal-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({
|
||||
appId: 'test-app-id',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-apps')
|
||||
const mockUseAppDetail = vi.mocked(useAppDetail)
|
||||
|
||||
type SortableItem = {
|
||||
id: string
|
||||
variable: PromptVariable
|
||||
@ -85,6 +96,18 @@ const createPromptVariable = (overrides: Partial<PromptVariable> = {}): PromptVa
|
||||
}
|
||||
}
|
||||
|
||||
function setupUseAppDetailMock() {
|
||||
mockUseAppDetail.mockReturnValue({
|
||||
data: {
|
||||
id: 'test-app-id',
|
||||
mode: AppModeEnum.CHAT,
|
||||
} as App,
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useAppDetail>)
|
||||
}
|
||||
|
||||
const renderConfigVar = (props: Partial<IConfigVarProps> = {}, debugOverrides: Partial<DebugConfigurationState> = {}) => {
|
||||
const defaultProps: IConfigVarProps = {
|
||||
promptVariables: [],
|
||||
@ -219,6 +242,7 @@ describe('ConfigVar', () => {
|
||||
subscriptionCallback = null
|
||||
variableIndex = 0
|
||||
notifySpy.mockClear()
|
||||
setupUseAppDetailMock()
|
||||
})
|
||||
|
||||
it('should save updates when editing a basic variable', async () => {
|
||||
|
||||
@ -15,11 +15,12 @@ vi.mock('next/navigation', () => ({
|
||||
push: mockPush,
|
||||
replace: mockReplace,
|
||||
}),
|
||||
useParams: () => ({ appId: 'app-123' }),
|
||||
}))
|
||||
|
||||
const mockSetAppDetail = vi.fn()
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (selector: (state: any) => unknown) => selector({ setAppDetail: mockSetAppDetail }),
|
||||
const mockInvalidateAppDetail = vi.fn()
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useInvalidateAppDetail: () => mockInvalidateAppDetail,
|
||||
}))
|
||||
|
||||
const mockSwitchApp = vi.fn()
|
||||
@ -275,7 +276,7 @@ describe('SwitchAppModal', () => {
|
||||
})
|
||||
expect(mockReplace).toHaveBeenCalledWith('/app/new-app-002/workflow')
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
expect(mockSetAppDetail).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAppDetail).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should notify error when switch app fails', async () => {
|
||||
|
||||
@ -34,6 +34,18 @@ import Logs from './index'
|
||||
|
||||
vi.mock('@/service/use-log')
|
||||
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useAppDetail: () => ({
|
||||
data: {
|
||||
id: 'test-app-id',
|
||||
name: 'Test App',
|
||||
mode: 'workflow',
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useDebounce: <T,>(value: T) => value,
|
||||
useDebounceFn: (fn: (value: string) => void) => ({ run: fn }),
|
||||
@ -51,6 +63,9 @@ vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
}),
|
||||
useParams: () => ({
|
||||
appId: 'test-app-id',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { MarketplaceCollection } from './types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { act, render, renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
|
||||
// ================================
|
||||
@ -157,6 +157,45 @@ vi.mock('@/config', () => ({
|
||||
MARKETPLACE_API_PREFIX: 'https://marketplace.dify.ai/api/v1',
|
||||
}))
|
||||
|
||||
// Mock service/client - configurable mock for testing
|
||||
const {
|
||||
mockMarketplaceCollections,
|
||||
mockMarketplaceCollectionPlugins,
|
||||
mockMarketplaceSearchAdvanced,
|
||||
} = vi.hoisted(() => {
|
||||
const mockMarketplaceCollections = vi.fn(() => Promise.resolve({
|
||||
data: {
|
||||
collections: [
|
||||
{ name: 'test-collection', label: 'Test Collection' },
|
||||
],
|
||||
},
|
||||
}))
|
||||
const mockMarketplaceCollectionPlugins = vi.fn(() => Promise.resolve({
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
],
|
||||
},
|
||||
}))
|
||||
const mockMarketplaceSearchAdvanced = vi.fn(() => Promise.resolve({
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
],
|
||||
bundles: [],
|
||||
total: 1,
|
||||
},
|
||||
}))
|
||||
return { mockMarketplaceCollections, mockMarketplaceCollectionPlugins, mockMarketplaceSearchAdvanced }
|
||||
})
|
||||
vi.mock('@/service/client', () => ({
|
||||
marketplaceClient: {
|
||||
collections: mockMarketplaceCollections,
|
||||
collectionPlugins: mockMarketplaceCollectionPlugins,
|
||||
searchAdvanced: mockMarketplaceSearchAdvanced,
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock var utils
|
||||
vi.mock('@/utils/var', () => ({
|
||||
getMarketplaceUrl: (path: string, _params?: Record<string, string | undefined>) => `https://marketplace.dify.ai${path}`,
|
||||
@ -199,7 +238,7 @@ vi.mock('@/i18n-config/language', () => ({
|
||||
}))
|
||||
|
||||
// Mock global fetch for utils testing
|
||||
const originalFetch = globalThis.fetch
|
||||
const _originalFetch = globalThis.fetch
|
||||
|
||||
// Mock useTags hook
|
||||
const mockTags = [
|
||||
@ -1477,25 +1516,33 @@ describe('flatMap Coverage', () => {
|
||||
describe('Async Utils', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
// Reset mocks to default behavior
|
||||
mockMarketplaceCollections.mockImplementation(() => Promise.resolve({
|
||||
data: {
|
||||
collections: [
|
||||
{ name: 'test-collection', label: 'Test Collection' },
|
||||
],
|
||||
},
|
||||
}))
|
||||
mockMarketplaceCollectionPlugins.mockImplementation(() => Promise.resolve({
|
||||
data: {
|
||||
plugins: [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
],
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
describe('getMarketplacePluginsByCollectionId', () => {
|
||||
it('should fetch plugins by collection id successfully', async () => {
|
||||
const mockPlugins = [
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1' },
|
||||
{ type: 'plugin', org: 'test', name: 'plugin2' },
|
||||
{ type: 'plugin', org: 'test', name: 'plugin1', tags: [] },
|
||||
{ type: 'plugin', org: 'test', name: 'plugin2', tags: [] },
|
||||
]
|
||||
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ data: { plugins: mockPlugins } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
mockMarketplaceCollectionPlugins.mockResolvedValue({
|
||||
data: { plugins: mockPlugins },
|
||||
})
|
||||
|
||||
const { getMarketplacePluginsByCollectionId } = await import('./utils')
|
||||
const result = await getMarketplacePluginsByCollectionId('test-collection', {
|
||||
@ -1504,12 +1551,12 @@ describe('Async Utils', () => {
|
||||
type: 'plugin',
|
||||
})
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalled()
|
||||
expect(mockMarketplaceCollectionPlugins).toHaveBeenCalled()
|
||||
expect(result).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should handle fetch error and return empty array', async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
mockMarketplaceCollectionPlugins.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const { getMarketplacePluginsByCollectionId } = await import('./utils')
|
||||
const result = await getMarketplacePluginsByCollectionId('test-collection')
|
||||
@ -1518,53 +1565,39 @@ describe('Async Utils', () => {
|
||||
})
|
||||
|
||||
it('should pass abort signal when provided', async () => {
|
||||
const mockPlugins = [{ type: 'plugins', org: 'test', name: 'plugin1' }]
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ data: { plugins: mockPlugins } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
const mockPlugins = [{ type: 'plugins', org: 'test', name: 'plugin1', tags: [] }]
|
||||
mockMarketplaceCollectionPlugins.mockResolvedValue({
|
||||
data: { plugins: mockPlugins },
|
||||
})
|
||||
|
||||
const controller = new AbortController()
|
||||
const { getMarketplacePluginsByCollectionId } = await import('./utils')
|
||||
await getMarketplacePluginsByCollectionId('test-collection', {}, { signal: controller.signal })
|
||||
|
||||
// oRPC uses Request objects, so check that fetch was called with a Request containing the right URL
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
expect.any(Request),
|
||||
expect.any(Object),
|
||||
// Check that collectionPlugins was called with the correct params including signal
|
||||
expect(mockMarketplaceCollectionPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
params: { collectionId: 'test-collection' },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
signal: controller.signal,
|
||||
}),
|
||||
)
|
||||
const call = vi.mocked(globalThis.fetch).mock.calls[0]
|
||||
const request = call[0] as Request
|
||||
expect(request.url).toContain('test-collection')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMarketplaceCollectionsAndPlugins', () => {
|
||||
it('should fetch collections and plugins successfully', async () => {
|
||||
const mockCollections = [
|
||||
{ name: 'collection1', label: {}, description: {}, rule: '', created_at: '', updated_at: '' },
|
||||
{ name: 'collection1', label: 'Collection 1' },
|
||||
]
|
||||
const mockPlugins = [{ type: 'plugins', org: 'test', name: 'plugin1' }]
|
||||
const mockPlugins = [{ type: 'plugins', org: 'test', name: 'plugin1', tags: [] }]
|
||||
|
||||
let callCount = 0
|
||||
globalThis.fetch = vi.fn().mockImplementation(() => {
|
||||
callCount++
|
||||
if (callCount === 1) {
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify({ data: { collections: mockCollections } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify({ data: { plugins: mockPlugins } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
mockMarketplaceCollections.mockResolvedValue({
|
||||
data: { collections: mockCollections },
|
||||
})
|
||||
mockMarketplaceCollectionPlugins.mockResolvedValue({
|
||||
data: { plugins: mockPlugins },
|
||||
})
|
||||
|
||||
const { getMarketplaceCollectionsAndPlugins } = await import('./utils')
|
||||
@ -1578,7 +1611,7 @@ describe('Async Utils', () => {
|
||||
})
|
||||
|
||||
it('should handle fetch error and return empty data', async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
mockMarketplaceCollections.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const { getMarketplaceCollectionsAndPlugins } = await import('./utils')
|
||||
const result = await getMarketplaceCollectionsAndPlugins()
|
||||
@ -1588,12 +1621,9 @@ describe('Async Utils', () => {
|
||||
})
|
||||
|
||||
it('should append condition and type to URL when provided', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ data: { collections: [] } }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
)
|
||||
mockMarketplaceCollections.mockResolvedValue({
|
||||
data: { collections: [] },
|
||||
})
|
||||
|
||||
const { getMarketplaceCollectionsAndPlugins } = await import('./utils')
|
||||
await getMarketplaceCollectionsAndPlugins({
|
||||
@ -1601,11 +1631,16 @@ describe('Async Utils', () => {
|
||||
type: 'bundle',
|
||||
})
|
||||
|
||||
// oRPC uses Request objects, so check that fetch was called with a Request containing the right URL
|
||||
expect(globalThis.fetch).toHaveBeenCalled()
|
||||
const call = vi.mocked(globalThis.fetch).mock.calls[0]
|
||||
const request = call[0] as Request
|
||||
expect(request.url).toContain('condition=category%3Dtool')
|
||||
// Check that collections was called with the correct query params
|
||||
expect(mockMarketplaceCollections).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: expect.objectContaining({
|
||||
condition: 'category=tool',
|
||||
type: 'bundle',
|
||||
}),
|
||||
}),
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -7,6 +7,10 @@ import { Plan } from '@/app/components/billing/type'
|
||||
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
|
||||
import FeaturesTrigger from './features-trigger'
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ appId: 'app-id' }),
|
||||
}))
|
||||
|
||||
const mockUseIsChatMode = vi.fn()
|
||||
const mockUseTheme = vi.fn()
|
||||
const mockUseNodesReadOnly = vi.fn()
|
||||
@ -26,8 +30,7 @@ const mockPublishWorkflow = vi.fn()
|
||||
const mockUpdatePublishedWorkflow = vi.fn()
|
||||
const mockResetWorkflowVersionHistory = vi.fn()
|
||||
const mockInvalidateAppTriggers = vi.fn()
|
||||
const mockFetchAppDetail = vi.fn()
|
||||
const mockSetAppDetail = vi.fn()
|
||||
const mockInvalidateAppDetail = vi.fn()
|
||||
const mockSetPublishedAt = vi.fn()
|
||||
const mockSetLastPublishedHasUserInput = vi.fn()
|
||||
|
||||
@ -126,8 +129,8 @@ vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidateAppTriggers: () => mockInvalidateAppTriggers,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/apps', () => ({
|
||||
fetchAppDetail: (...args: unknown[]) => mockFetchAppDetail(...args),
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useInvalidateAppDetail: () => mockInvalidateAppDetail,
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
@ -135,7 +138,7 @@ vi.mock('@/hooks/use-theme', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (selector: (state: { appDetail?: { id: string }, setAppDetail: typeof mockSetAppDetail }) => unknown) => mockUseAppStoreSelector(selector),
|
||||
useStore: (selector: (state: { appDetail?: { id: string } }) => unknown) => mockUseAppStoreSelector(selector),
|
||||
}))
|
||||
|
||||
const createProviderContext = ({
|
||||
@ -178,8 +181,7 @@ describe('FeaturesTrigger', () => {
|
||||
mockUseProviderContext.mockReturnValue(createProviderContext({}))
|
||||
mockUseNodes.mockReturnValue([])
|
||||
mockUseEdges.mockReturnValue([])
|
||||
mockUseAppStoreSelector.mockImplementation(selector => selector({ appDetail: { id: 'app-id' }, setAppDetail: mockSetAppDetail }))
|
||||
mockFetchAppDetail.mockResolvedValue({ id: 'app-id' })
|
||||
mockUseAppStoreSelector.mockImplementation(selector => selector({ appDetail: { id: 'app-id' } }))
|
||||
mockPublishWorkflow.mockResolvedValue({ created_at: '2024-01-01T00:00:00Z' })
|
||||
})
|
||||
|
||||
@ -423,8 +425,7 @@ describe('FeaturesTrigger', () => {
|
||||
expect(mockSetLastPublishedHasUserInput).toHaveBeenCalledWith(true)
|
||||
expect(mockResetWorkflowVersionHistory).toHaveBeenCalled()
|
||||
expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'common.api.actionSuccess' })
|
||||
expect(mockFetchAppDetail).toHaveBeenCalledWith({ url: '/apps', id: 'app-id' })
|
||||
expect(mockSetAppDetail).toHaveBeenCalled()
|
||||
expect(mockInvalidateAppDetail).toHaveBeenCalledWith('app-id')
|
||||
})
|
||||
})
|
||||
|
||||
@ -445,23 +446,5 @@ describe('FeaturesTrigger', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should log error when app detail refresh fails after publish', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
||||
mockFetchAppDetail.mockRejectedValue(new Error('fetch failed'))
|
||||
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: 'publisher-publish' }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalled()
|
||||
})
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,10 +4,10 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import WorkflowHeader from './index'
|
||||
|
||||
const mockUseAppStoreSelector = vi.fn()
|
||||
const mockSetCurrentLogItem = vi.fn()
|
||||
const mockSetShowMessageLogModal = vi.fn()
|
||||
const mockResetWorkflowVersionHistory = vi.fn()
|
||||
const mockUseAppDetail = vi.fn()
|
||||
|
||||
const createMockApp = (overrides: Partial<App> = {}): App => ({
|
||||
id: 'app-id',
|
||||
@ -39,19 +39,24 @@ const createMockApp = (overrides: Partial<App> = {}): App => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
let appDetail: App
|
||||
|
||||
const mockAppStore = (overrides: Partial<App> = {}) => {
|
||||
appDetail = createMockApp(overrides)
|
||||
mockUseAppStoreSelector.mockImplementation(selector => selector({
|
||||
appDetail,
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
}))
|
||||
const appDetail = createMockApp(overrides)
|
||||
mockUseAppDetail.mockReturnValue({ data: appDetail })
|
||||
}
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ appId: 'app-id' }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-apps', () => ({
|
||||
useAppDetail: (...args: unknown[]) => mockUseAppDetail(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (selector: (state: { appDetail?: App, setCurrentLogItem: typeof mockSetCurrentLogItem, setShowMessageLogModal: typeof mockSetShowMessageLogModal }) => unknown) => mockUseAppStoreSelector(selector),
|
||||
useStore: (selector: (state: { setCurrentLogItem: typeof mockSetCurrentLogItem, setShowMessageLogModal: typeof mockSetShowMessageLogModal }) => unknown) => selector({
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/header', () => ({
|
||||
|
||||
@ -608,11 +608,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/app/switch-app-modal/index.spec.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/app/switch-app-modal/index.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
|
||||
Reference in New Issue
Block a user