Merge branch 'feature/beta-release' into renovate/feature/beta-release-non-major-updates
This commit is contained in:
@@ -265,3 +265,20 @@ func TestDeleteWhitelist_ReloadFailure(t *testing.T) {
|
||||
assert.Equal(t, http.StatusNoContent, w.Code)
|
||||
assert.True(t, mock.reloadCalled)
|
||||
}
|
||||
|
||||
func TestDeleteWhitelist_EmptyUUID(t *testing.T) {
|
||||
t.Parallel()
|
||||
h, _, _ := setupWhitelistHandler(t)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodDelete, "/api/v1/admin/crowdsec/whitelist/", nil)
|
||||
c.Params = gin.Params{{Key: "uuid", Value: ""}}
|
||||
|
||||
h.DeleteWhitelist(c)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(t, "uuid is required", resp["error"])
|
||||
}
|
||||
|
||||
@@ -300,3 +300,10 @@ func TestCrowdSecWhitelistService_WriteYAML_RenameError(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "rename")
|
||||
}
|
||||
|
||||
func TestCrowdSecWhitelistService_Add_InvalidCIDR(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := services.NewCrowdSecWhitelistService(openWhitelistTestDB(t), "")
|
||||
_, err := svc.Add(context.Background(), "not-an-ip/24", "invalid cidr with slash")
|
||||
assert.ErrorIs(t, err, services.ErrInvalidIPOrCIDR)
|
||||
}
|
||||
|
||||
28
frontend/package-lock.json
generated
28
frontend/package-lock.json
generated
@@ -118,9 +118,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.0.tgz",
|
||||
"integrity": "sha512-ASf825+5vsGuYWoyFyNsex2mNtPTXpCvYTR942+w/eNw7PqS0Lhl/PE1hC7bajneI3m/Oxi+yrP3vTOPxfwM8A==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
|
||||
"integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4978,9 +4978,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.19",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",
|
||||
"integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==",
|
||||
"version": "2.10.20",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz",
|
||||
"integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -5831,13 +5831,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
||||
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
@@ -9843,13 +9843,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
||||
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
||||
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
"entities": "^8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
|
||||
@@ -116,6 +116,120 @@ describe('crowdsec API', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('listCrowdsecDecisions', () => {
|
||||
it('should call GET /admin/crowdsec/decisions and return data', async () => {
|
||||
const mockData = {
|
||||
decisions: [
|
||||
{ id: '1', ip: '1.2.3.4', reason: 'bot', duration: '24h', created_at: '2024-01-01T00:00:00Z', source: 'crowdsec' },
|
||||
],
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await crowdsec.listCrowdsecDecisions()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/decisions')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('banIP', () => {
|
||||
it('should call POST /admin/crowdsec/ban with ip, duration, and reason', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({})
|
||||
|
||||
await crowdsec.banIP('1.2.3.4', '24h', 'manual ban')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/ban', {
|
||||
ip: '1.2.3.4',
|
||||
duration: '24h',
|
||||
reason: 'manual ban',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('unbanIP', () => {
|
||||
it('should call DELETE /admin/crowdsec/ban/{encoded ip}', async () => {
|
||||
vi.mocked(client.delete).mockResolvedValue({})
|
||||
|
||||
await crowdsec.unbanIP('1.2.3.4')
|
||||
|
||||
expect(client.delete).toHaveBeenCalledWith('/admin/crowdsec/ban/1.2.3.4')
|
||||
})
|
||||
|
||||
it('should URL-encode special characters in the IP', async () => {
|
||||
vi.mocked(client.delete).mockResolvedValue({})
|
||||
|
||||
await crowdsec.unbanIP('::1')
|
||||
|
||||
expect(client.delete).toHaveBeenCalledWith('/admin/crowdsec/ban/%3A%3A1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCrowdsecKeyStatus', () => {
|
||||
it('should call GET /admin/crowdsec/key-status and return data', async () => {
|
||||
const mockData = {
|
||||
key_source: 'file' as const,
|
||||
env_key_rejected: false,
|
||||
current_key_preview: 'abc***xyz',
|
||||
message: 'Key loaded from file',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await crowdsec.getCrowdsecKeyStatus()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/key-status')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('listWhitelists', () => {
|
||||
it('should call GET /admin/crowdsec/whitelist and return the whitelist array', async () => {
|
||||
const mockWhitelist = [
|
||||
{
|
||||
uuid: 'uuid-1',
|
||||
ip_or_cidr: '192.168.1.1',
|
||||
reason: 'Home',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
]
|
||||
vi.mocked(client.get).mockResolvedValue({ data: { whitelist: mockWhitelist } })
|
||||
|
||||
const result = await crowdsec.listWhitelists()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/whitelist')
|
||||
expect(result).toEqual(mockWhitelist)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addWhitelist', () => {
|
||||
it('should call POST /admin/crowdsec/whitelist and return the created entry', async () => {
|
||||
const payload = { ip_or_cidr: '192.168.1.1', reason: 'Home' }
|
||||
const mockEntry = {
|
||||
uuid: 'uuid-1',
|
||||
ip_or_cidr: '192.168.1.1',
|
||||
reason: 'Home',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockEntry })
|
||||
|
||||
const result = await crowdsec.addWhitelist(payload)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/whitelist', payload)
|
||||
expect(result).toEqual(mockEntry)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteWhitelist', () => {
|
||||
it('should call DELETE /admin/crowdsec/whitelist/{uuid}', async () => {
|
||||
vi.mocked(client.delete).mockResolvedValue({})
|
||||
|
||||
await crowdsec.deleteWhitelist('uuid-1')
|
||||
|
||||
expect(client.delete).toHaveBeenCalledWith('/admin/crowdsec/whitelist/uuid-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('default export', () => {
|
||||
it('should export all functions', () => {
|
||||
expect(crowdsec.default).toHaveProperty('startCrowdsec')
|
||||
@@ -126,6 +240,14 @@ describe('crowdsec API', () => {
|
||||
expect(crowdsec.default).toHaveProperty('listCrowdsecFiles')
|
||||
expect(crowdsec.default).toHaveProperty('readCrowdsecFile')
|
||||
expect(crowdsec.default).toHaveProperty('writeCrowdsecFile')
|
||||
expect(crowdsec.default).toHaveProperty('listCrowdsecDecisions')
|
||||
expect(crowdsec.default).toHaveProperty('banIP')
|
||||
expect(crowdsec.default).toHaveProperty('unbanIP')
|
||||
expect(crowdsec.default).toHaveProperty('getCrowdsecKeyStatus')
|
||||
expect(crowdsec.default).toHaveProperty('listWhitelists')
|
||||
expect(crowdsec.default).toHaveProperty('addWhitelist')
|
||||
expect(crowdsec.default).toHaveProperty('deleteWhitelist')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
321
frontend/src/pages/__tests__/CrowdSecConfig.whitelist.test.tsx
Normal file
321
frontend/src/pages/__tests__/CrowdSecConfig.whitelist.test.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { AxiosError } from 'axios'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import * as backupsApi from '../../api/backups'
|
||||
import * as crowdsecApi from '../../api/crowdsec'
|
||||
import * as featureFlagsApi from '../../api/featureFlags'
|
||||
import * as presetsApi from '../../api/presets'
|
||||
import * as securityApi from '../../api/security'
|
||||
import * as settingsApi from '../../api/settings'
|
||||
import * as systemApi from '../../api/system'
|
||||
import { renderWithQueryClient } from '../../test-utils/renderWithQueryClient'
|
||||
import { toast } from '../../utils/toast'
|
||||
import CrowdSecConfig from '../CrowdSecConfig'
|
||||
|
||||
vi.mock('../../api/security')
|
||||
vi.mock('../../api/crowdsec')
|
||||
vi.mock('../../api/presets')
|
||||
vi.mock('../../api/backups')
|
||||
vi.mock('../../api/settings')
|
||||
vi.mock('../../api/featureFlags')
|
||||
vi.mock('../../api/system')
|
||||
vi.mock('../../hooks/useConsoleEnrollment', () => ({
|
||||
useConsoleStatus: vi.fn(() => ({
|
||||
data: {
|
||||
status: 'not_enrolled',
|
||||
key_present: false,
|
||||
last_error: null,
|
||||
last_attempt_at: null,
|
||||
enrolled_at: null,
|
||||
last_heartbeat_at: null,
|
||||
correlation_id: 'corr-1',
|
||||
tenant: 'default',
|
||||
agent_name: 'charon-agent',
|
||||
},
|
||||
isLoading: false,
|
||||
isRefetching: false,
|
||||
})),
|
||||
useEnrollConsole: vi.fn(() => ({
|
||||
mutateAsync: vi.fn().mockResolvedValue({ status: 'enrolling', key_present: false }),
|
||||
isPending: false,
|
||||
})),
|
||||
useClearConsoleEnrollment: vi.fn(() => ({ mutate: vi.fn(), isPending: false })),
|
||||
}))
|
||||
vi.mock('../../components/CrowdSecBouncerKeyDisplay', () => ({
|
||||
CrowdSecBouncerKeyDisplay: () => null,
|
||||
}))
|
||||
vi.mock('../../utils/crowdsecExport', () => ({
|
||||
buildCrowdsecExportFilename: vi.fn(() => 'crowdsec-default.tar.gz'),
|
||||
promptCrowdsecFilename: vi.fn(() => 'crowdsec.tar.gz'),
|
||||
downloadCrowdsecExport: vi.fn(),
|
||||
}))
|
||||
vi.mock('../../utils/toast', () => ({
|
||||
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() },
|
||||
}))
|
||||
|
||||
// The i18n mock in test setup returns the translation key when no translation is found.
|
||||
// These constants keep assertions in sync with what the component actually renders.
|
||||
const TAB_WHITELIST = 'crowdsecConfig.whitelist.tabLabel'
|
||||
const MODAL_TITLE = 'crowdsecConfig.whitelist.deleteModal.title'
|
||||
const BTN_REMOVE = 'crowdsecConfig.whitelist.deleteModal.submit'
|
||||
|
||||
const baseStatus = {
|
||||
cerberus: { enabled: true },
|
||||
crowdsec: { enabled: true, mode: 'local' as const, api_url: '' },
|
||||
waf: { enabled: true, mode: 'enabled' as const },
|
||||
rate_limit: { enabled: true },
|
||||
acl: { enabled: true },
|
||||
}
|
||||
|
||||
const axiosError = (status: number, message: string, data?: Record<string, unknown>) =>
|
||||
new AxiosError(message, undefined, undefined, undefined, {
|
||||
status,
|
||||
statusText: String(status),
|
||||
headers: {},
|
||||
config: {},
|
||||
data: data ?? { error: message },
|
||||
} as never)
|
||||
|
||||
const mockWhitelistEntries = [
|
||||
{
|
||||
uuid: 'uuid-1',
|
||||
ip_or_cidr: '192.168.1.1',
|
||||
reason: 'Home IP',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
uuid: 'uuid-2',
|
||||
ip_or_cidr: '10.0.0.0/8',
|
||||
reason: 'LAN',
|
||||
created_at: '2024-01-02T00:00:00Z',
|
||||
updated_at: '2024-01-02T00:00:00Z',
|
||||
},
|
||||
]
|
||||
|
||||
const renderPage = async () => {
|
||||
renderWithQueryClient(<CrowdSecConfig />)
|
||||
await waitFor(() => screen.getByText('CrowdSec Configuration'))
|
||||
}
|
||||
|
||||
const goToWhitelistTab = async () => {
|
||||
await userEvent.click(screen.getByRole('tab', { name: TAB_WHITELIST }))
|
||||
await waitFor(() => screen.getByTestId('whitelist-ip-input'))
|
||||
}
|
||||
|
||||
describe('CrowdSecConfig – whitelist tab', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(baseStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 123, lapi_ready: true })
|
||||
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: ['acquis.yaml'] })
|
||||
vi.mocked(crowdsecApi.readCrowdsecFile).mockResolvedValue({ content: '' })
|
||||
vi.mocked(crowdsecApi.writeCrowdsecFile).mockResolvedValue(undefined)
|
||||
vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValue({ decisions: [] })
|
||||
vi.mocked(crowdsecApi.banIP).mockResolvedValue(undefined)
|
||||
vi.mocked(crowdsecApi.unbanIP).mockResolvedValue(undefined)
|
||||
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(new Blob(['data']))
|
||||
vi.mocked(crowdsecApi.importCrowdsecConfig).mockResolvedValue(undefined)
|
||||
vi.mocked(crowdsecApi.listWhitelists).mockResolvedValue([])
|
||||
vi.mocked(crowdsecApi.addWhitelist).mockResolvedValue({
|
||||
uuid: 'uuid-new',
|
||||
ip_or_cidr: '1.2.3.4',
|
||||
reason: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
})
|
||||
vi.mocked(crowdsecApi.deleteWhitelist).mockResolvedValue(undefined)
|
||||
vi.mocked(presetsApi.listCrowdsecPresets).mockResolvedValue({ presets: [] })
|
||||
vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValue({
|
||||
status: 'pulled',
|
||||
slug: '',
|
||||
preview: '',
|
||||
cache_key: '',
|
||||
etag: '',
|
||||
retrieved_at: '',
|
||||
source: 'hub',
|
||||
})
|
||||
vi.mocked(presetsApi.applyCrowdsecPreset).mockResolvedValue({
|
||||
status: 'applied',
|
||||
backup: '',
|
||||
reload_hint: false,
|
||||
used_cscli: false,
|
||||
cache_key: '',
|
||||
slug: '',
|
||||
})
|
||||
vi.mocked(presetsApi.getCrowdsecPresetCache).mockResolvedValue({
|
||||
preview: '',
|
||||
cache_key: '',
|
||||
etag: '',
|
||||
})
|
||||
vi.mocked(backupsApi.createBackup).mockResolvedValue({ filename: 'backup.tar.gz' })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue({
|
||||
'feature.crowdsec.console_enrollment': false,
|
||||
})
|
||||
vi.mocked(systemApi.getMyIP).mockResolvedValue({ ip: '203.0.113.1', source: 'cloudflare' })
|
||||
})
|
||||
|
||||
it('shows whitelist tab trigger in local mode', async () => {
|
||||
await renderPage()
|
||||
expect(screen.getByRole('tab', { name: TAB_WHITELIST })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show whitelist tab in disabled mode', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue({
|
||||
...baseStatus,
|
||||
crowdsec: { enabled: true, mode: 'disabled' as const, api_url: '' },
|
||||
})
|
||||
await renderPage()
|
||||
expect(screen.queryByRole('tab', { name: TAB_WHITELIST })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows empty state when there are no whitelist entries', async () => {
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
expect(screen.getByTestId('whitelist-empty')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders whitelist entries in the table', async () => {
|
||||
vi.mocked(crowdsecApi.listWhitelists).mockResolvedValue(mockWhitelistEntries)
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
expect(await screen.findByText('192.168.1.1')).toBeInTheDocument()
|
||||
expect(screen.getByText('10.0.0.0/8')).toBeInTheDocument()
|
||||
expect(screen.getByText('Home IP')).toBeInTheDocument()
|
||||
expect(screen.getByText('LAN')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('submits a new whitelist entry', async () => {
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
await userEvent.type(screen.getByTestId('whitelist-ip-input'), '1.2.3.4')
|
||||
await userEvent.type(screen.getByTestId('whitelist-reason-input'), 'Test reason')
|
||||
await userEvent.click(screen.getByTestId('whitelist-add-btn'))
|
||||
await waitFor(() =>
|
||||
expect(crowdsecApi.addWhitelist).toHaveBeenCalledWith({
|
||||
ip_or_cidr: '1.2.3.4',
|
||||
reason: 'Test reason',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows add-whitelist loading overlay while mutation is pending', async () => {
|
||||
let resolveAdd!: (v: (typeof mockWhitelistEntries)[0]) => void
|
||||
vi.mocked(crowdsecApi.addWhitelist).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveAdd = resolve
|
||||
}),
|
||||
)
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
await userEvent.type(screen.getByTestId('whitelist-ip-input'), '1.2.3.4')
|
||||
await userEvent.click(screen.getByTestId('whitelist-add-btn'))
|
||||
await waitFor(() => expect(screen.getByText('Adding IP to whitelist')).toBeInTheDocument())
|
||||
resolveAdd(mockWhitelistEntries[0])
|
||||
})
|
||||
|
||||
it('displays inline error when adding a whitelist entry fails', async () => {
|
||||
vi.mocked(crowdsecApi.addWhitelist).mockRejectedValueOnce(
|
||||
axiosError(400, 'Invalid IP', { error: 'bad ip format' }),
|
||||
)
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
await userEvent.type(screen.getByTestId('whitelist-ip-input'), 'bad-ip')
|
||||
await userEvent.click(screen.getByTestId('whitelist-add-btn'))
|
||||
await waitFor(() => expect(screen.getByTestId('whitelist-ip-error')).toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('opens delete confirmation dialog', async () => {
|
||||
vi.mocked(crowdsecApi.listWhitelists).mockResolvedValue(mockWhitelistEntries)
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
await userEvent.click((await screen.findAllByTestId('whitelist-delete-btn'))[0])
|
||||
expect(await screen.findByRole('dialog', { name: MODAL_TITLE })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('cancels whitelist deletion via Cancel button', async () => {
|
||||
vi.mocked(crowdsecApi.listWhitelists).mockResolvedValue(mockWhitelistEntries)
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
await userEvent.click((await screen.findAllByTestId('whitelist-delete-btn'))[0])
|
||||
await userEvent.click(await screen.findByRole('button', { name: 'Cancel' }))
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog', { name: MODAL_TITLE })).not.toBeInTheDocument(),
|
||||
)
|
||||
expect(crowdsecApi.deleteWhitelist).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('confirms whitelist entry deletion via Remove button', async () => {
|
||||
vi.mocked(crowdsecApi.listWhitelists).mockResolvedValue(mockWhitelistEntries)
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
await userEvent.click((await screen.findAllByTestId('whitelist-delete-btn'))[0])
|
||||
await userEvent.click(await screen.findByRole('button', { name: BTN_REMOVE }))
|
||||
await waitFor(() => expect(crowdsecApi.deleteWhitelist).toHaveBeenCalledWith('uuid-1'))
|
||||
})
|
||||
|
||||
it('shows delete-whitelist loading overlay while mutation is pending', async () => {
|
||||
vi.mocked(crowdsecApi.listWhitelists).mockResolvedValue(mockWhitelistEntries)
|
||||
let resolveDelete!: () => void
|
||||
vi.mocked(crowdsecApi.deleteWhitelist).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveDelete = resolve
|
||||
}),
|
||||
)
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
await userEvent.click((await screen.findAllByTestId('whitelist-delete-btn'))[0])
|
||||
await userEvent.click(await screen.findByRole('button', { name: BTN_REMOVE }))
|
||||
await waitFor(() => expect(screen.getByText('Removing from whitelist')).toBeInTheDocument())
|
||||
resolveDelete()
|
||||
})
|
||||
|
||||
it('closes delete dialog on Escape key', async () => {
|
||||
vi.mocked(crowdsecApi.listWhitelists).mockResolvedValue(mockWhitelistEntries)
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
await userEvent.click((await screen.findAllByTestId('whitelist-delete-btn'))[0])
|
||||
expect(await screen.findByRole('dialog', { name: MODAL_TITLE })).toBeInTheDocument()
|
||||
await userEvent.keyboard('{Escape}')
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog', { name: MODAL_TITLE })).not.toBeInTheDocument(),
|
||||
)
|
||||
})
|
||||
|
||||
it('closes delete dialog when backdrop is clicked', async () => {
|
||||
vi.mocked(crowdsecApi.listWhitelists).mockResolvedValue(mockWhitelistEntries)
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
await userEvent.click((await screen.findAllByTestId('whitelist-delete-btn'))[0])
|
||||
expect(await screen.findByRole('dialog', { name: MODAL_TITLE })).toBeInTheDocument()
|
||||
await userEvent.click(screen.getByRole('button', { name: /close/i }))
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog', { name: MODAL_TITLE })).not.toBeInTheDocument(),
|
||||
)
|
||||
})
|
||||
|
||||
it('fills IP input when Add My IP is clicked', async () => {
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
await userEvent.click(screen.getByTestId('whitelist-add-my-ip-btn'))
|
||||
await waitFor(() => {
|
||||
const input = screen.getByTestId('whitelist-ip-input') as HTMLInputElement
|
||||
expect(input.value).toBe('203.0.113.1')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows error toast when Add My IP request fails', async () => {
|
||||
vi.mocked(systemApi.getMyIP).mockRejectedValueOnce(new Error('network error'))
|
||||
await renderPage()
|
||||
await goToWhitelistTab()
|
||||
await userEvent.click(screen.getByTestId('whitelist-add-my-ip-btn'))
|
||||
await waitFor(() =>
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to detect your IP address'),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -27,7 +27,7 @@ vi.mock('../../hooks/useRemoteServers', () => ({
|
||||
vi.mock('../../hooks/useCertificates', () => ({
|
||||
useCertificates: () => ({
|
||||
certificates: [
|
||||
{ id: 1, status: 'valid', domain: 'test.com' },
|
||||
{ id: 1, status: 'valid', domain: 'test.com', domains: 'test.com,www.test.com' },
|
||||
{ id: 2, status: 'expired', domain: 'expired.com' },
|
||||
],
|
||||
isLoading: false,
|
||||
@@ -84,4 +84,5 @@ describe('Dashboard page', () => {
|
||||
// "1 valid" still renders even though cert.domains is undefined
|
||||
expect(screen.getByText('1 valid')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user