test: add unit tests for Uptime page and setup API

This commit is contained in:
GitHub Actions
2025-11-30 15:39:21 +00:00
parent 224a53975d
commit 92697ec5ec
4 changed files with 214 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import client from '../../api/client'
import { getSetupStatus, performSetup } from '../setup'
describe('setup api', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
it('getSetupStatus returns status', async () => {
const data = { setupRequired: true }
vi.spyOn(client, 'get').mockResolvedValueOnce({ data })
const res = await getSetupStatus()
expect(res).toEqual(data)
})
it('performSetup posts data to setup endpoint', async () => {
const spy = vi.spyOn(client, 'post').mockResolvedValueOnce({ data: {} })
const payload = { name: 'Admin', email: 'admin@example.com', password: 'secret' }
await performSetup(payload)
expect(spy).toHaveBeenCalledWith('/setup', payload)
})
})

View File

@@ -0,0 +1,26 @@
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import { AuthContext } from '../../context/AuthContextValue'
import { useAuth } from '../useAuth'
const TestComponent = () => {
const auth = useAuth()
return <div>{auth.isAuthenticated ? 'auth' : 'no-auth'}</div>
}
describe('useAuth hook', () => {
it('throws if used outside provider', () => {
const renderOutside = () => render(<TestComponent />)
expect(renderOutside).toThrowError('useAuth must be used within an AuthProvider')
})
it('returns context inside provider', () => {
const fakeCtx = { user: { user_id: 1, role: 'admin', name: 'Test', email: 't@example.com' }, login: async () => {}, logout: () => {}, changePassword: async () => {}, isAuthenticated: true, isLoading: false }
render(
<AuthContext.Provider value={fakeCtx}>
<TestComponent />
</AuthContext.Provider>
)
expect(screen.getByText('auth')).toBeTruthy()
})
})

View File

@@ -48,4 +48,153 @@ describe('Uptime page', () => {
await userEvent.click(toggleBtn)
await waitFor(() => expect(uptimeApi.updateMonitor).toHaveBeenCalledWith('m1', { enabled: false }))
})
it('shows Never when last_check is missing', async () => {
const monitor = {
id: 'm2', name: 'NoLastCheck', url: 'http://example.com', type: 'http', interval: 60, enabled: true,
status: 'up', last_check: null, latency: 10, max_retries: 3,
}
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor])
vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([])
renderWithProviders(<Uptime />)
await waitFor(() => expect(screen.getByText('NoLastCheck')).toBeInTheDocument())
const lastCheck = screen.getByText('Never')
expect(lastCheck).toBeTruthy()
})
it('shows PAUSED state when monitor is disabled', async () => {
const monitor = {
id: 'm3', name: 'PausedMonitor', url: 'http://example.com', type: 'http', interval: 60, enabled: false,
status: 'down', last_check: new Date().toISOString(), latency: 10, max_retries: 3,
}
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor])
vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([])
renderWithProviders(<Uptime />)
await waitFor(() => expect(screen.getByText('PausedMonitor')).toBeInTheDocument())
expect(screen.getByText('PAUSED')).toBeTruthy()
})
it('renders heartbeat bars from history and displays status in bar titles', async () => {
const monitor = {
id: 'm4', name: 'WithHistory', url: 'http://example.com', type: 'http', interval: 60, enabled: true,
status: 'up', last_check: new Date().toISOString(), latency: 10, max_retries: 3,
}
const now = new Date()
const history = [
{ id: 1, monitor_id: 'm4', status: 'up', latency: 10, message: 'OK', created_at: new Date(now.getTime() - 30000).toISOString() },
{ id: 2, monitor_id: 'm4', status: 'down', latency: 20, message: 'Fail', created_at: new Date(now.getTime() - 20000).toISOString() },
{ id: 3, monitor_id: 'm4', status: 'up', latency: 5, message: 'OK', created_at: new Date(now.getTime() - 10000).toISOString() },
]
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor])
vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue(history)
renderWithProviders(<Uptime />)
await waitFor(() => expect(screen.getByText('WithHistory')).toBeInTheDocument())
// Bar titles include 'Status:' and the status should be capitalized
await waitFor(() => expect(document.querySelectorAll('[title*="Status:"]').length).toBeGreaterThanOrEqual(history.length))
const barTitles = Array.from(document.querySelectorAll('[title*="Status:"]'))
expect(barTitles.some(el => (el.getAttribute('title') || '').includes('Status: UP'))).toBeTruthy()
expect(barTitles.some(el => (el.getAttribute('title') || '').includes('Status: DOWN'))).toBeTruthy()
})
it('deletes monitor when delete confirmed and shows toast', async () => {
const monitor = {
id: 'm5', name: 'DeleteMe', url: 'http://example.com', type: 'http', interval: 60, enabled: true,
status: 'up', last_check: new Date().toISOString(), latency: 10, max_retries: 3,
}
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor])
vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([])
vi.mocked(uptimeApi.deleteMonitor).mockResolvedValue(undefined)
const confirmSpy = vi.spyOn(window, 'confirm').mockImplementation(() => true)
renderWithProviders(<Uptime />)
await waitFor(() => expect(screen.getByText('DeleteMe')).toBeInTheDocument())
const card = screen.getByText('DeleteMe').closest('div') as HTMLElement
const settingsBtn = within(card).getByTitle('Monitor settings')
await userEvent.click(settingsBtn)
const deleteBtn = within(card).getByText('Delete')
await userEvent.click(deleteBtn)
await waitFor(() => expect(uptimeApi.deleteMonitor).toHaveBeenCalledWith('m5'))
confirmSpy.mockRestore()
})
it('opens configure modal and saves changes via updateMonitor', async () => {
const monitor = {
id: 'm6', name: 'ConfigMe', url: 'http://example.com', type: 'http', interval: 60, enabled: true,
status: 'up', last_check: new Date().toISOString(), latency: 10, max_retries: 3, proxy_host_id: 1,
}
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor])
vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([])
vi.mocked(uptimeApi.updateMonitor).mockResolvedValue({ ...monitor, max_retries: 6 })
renderWithProviders(<Uptime />)
await waitFor(() => expect(screen.getByText('ConfigMe')).toBeInTheDocument())
const card = screen.getByText('ConfigMe').closest('div') as HTMLElement
await userEvent.click(within(card).getByTitle('Monitor settings'))
await userEvent.click(within(card).getByText('Configure'))
// Modal should open
await waitFor(() => expect(screen.getByText('Configure Monitor')).toBeInTheDocument())
const spinbuttons = screen.getAllByRole('spinbutton')
const maxRetriesInput = spinbuttons.find(el => el.getAttribute('value') === '3') as HTMLInputElement
await userEvent.clear(maxRetriesInput)
await userEvent.type(maxRetriesInput, '6')
await userEvent.click(screen.getByText('Save Changes'))
await waitFor(() => expect(uptimeApi.updateMonitor).toHaveBeenCalledWith('m6', { max_retries: 6, interval: 60 }))
})
it('does not call deleteMonitor when canceling delete', async () => {
const monitor = {
id: 'm7', name: 'DoNotDelete', url: 'http://example.com', type: 'http', interval: 60, enabled: true,
status: 'up', last_check: new Date().toISOString(), latency: 10, max_retries: 3,
}
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor])
vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([])
vi.mocked(uptimeApi.deleteMonitor).mockResolvedValue(undefined)
const confirmSpy = vi.spyOn(window, 'confirm').mockImplementation(() => false)
renderWithProviders(<Uptime />)
await waitFor(() => expect(screen.getByText('DoNotDelete')).toBeInTheDocument())
const card = screen.getByText('DoNotDelete').closest('div') as HTMLElement
await userEvent.click(within(card).getByTitle('Monitor settings'))
await userEvent.click(within(card).getByText('Delete'))
expect(uptimeApi.deleteMonitor).not.toHaveBeenCalled()
confirmSpy.mockRestore()
})
it('shows toast error when toggle update fails', async () => {
const monitor = {
id: 'm8', name: 'ToggleFail', url: 'http://example.com', type: 'http', interval: 60, enabled: true,
status: 'up', last_check: new Date().toISOString(), latency: 10, max_retries: 3, proxy_host_id: 1,
}
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor])
vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([])
vi.mocked(uptimeApi.updateMonitor).mockRejectedValue(new Error('Update failed'))
renderWithProviders(<Uptime />)
await waitFor(() => expect(screen.getByText('ToggleFail')).toBeInTheDocument())
const card = screen.getByText('ToggleFail').closest('div') as HTMLElement
await userEvent.click(within(card).getByTitle('Monitor settings'))
await userEvent.click(within(card).getByText('Disable Monitoring'))
const toast = (await import('react-hot-toast')).toast
await waitFor(() => expect(toast.error).toHaveBeenCalled())
})
it('separates monitors into Proxy Hosts, Remote Servers and Other sections', async () => {
const proxyMonitor = { id: 'm9', name: 'ProxyMon', url: 'http://p', type: 'http', interval: 60, enabled: true, status: 'up', last_check: new Date().toISOString(), latency: 1, max_retries: 2, proxy_host_id: 1 }
const remoteMonitor = { id: 'm10', name: 'RemoteMon', url: 'http://r', type: 'http', interval: 60, enabled: true, status: 'up', last_check: new Date().toISOString(), latency: 2, max_retries: 2, remote_server_id: 2 }
const otherMonitor = { id: 'm11', name: 'OtherMon', url: 'http://o', type: 'http', interval: 60, enabled: true, status: 'up', last_check: new Date().toISOString(), latency: 3, max_retries: 2 }
vi.mocked(uptimeApi.getMonitors).mockResolvedValue([proxyMonitor, remoteMonitor, otherMonitor])
vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([])
renderWithProviders(<Uptime />)
await waitFor(() => expect(screen.getByText('Proxy Hosts')).toBeInTheDocument())
expect(screen.getByText('Remote Servers')).toBeInTheDocument()
expect(screen.getByText('Other Monitors')).toBeInTheDocument()
expect(screen.getByText('ProxyMon')).toBeInTheDocument()
expect(screen.getByText('RemoteMon')).toBeInTheDocument()
expect(screen.getByText('OtherMon')).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest'
describe('Test setup file checks', () => {
it('sets the React act environment flag', () => {
expect(globalThis.IS_REACT_ACT_ENVIRONMENT).toBe(true)
})
it('stubs window.matchMedia with expected interface', () => {
const mq = window.matchMedia('(min-width: 100px)')
expect(mq.matches).toBe(false)
expect(typeof mq.addListener).toBe('function')
expect(typeof mq.removeListener).toBe('function')
expect(typeof mq.addEventListener).toBe('function')
expect(typeof mq.removeEventListener).toBe('function')
})
})