Complete lint remediation addressing errcheck, gosec, and staticcheck violations across backend test files. Tighten pre-commit configuration to prevent future blind spots. Key Changes: - Fix 61 Go linting issues (errcheck, gosec G115/G301/G304/G306, bodyclose) - Add proper error handling for json.Unmarshal, os.Setenv, db.Close(), w.Write() - Fix gosec G115 integer overflow with strconv.FormatUint - Add #nosec annotations with justifications for test fixtures - Fix SecurityService goroutine leaks (add Close() calls) - Fix CrowdSec tar.gz non-deterministic ordering with sorted keys Pre-commit Hardening: - Remove test file exclusion from golangci-lint hook - Add gosec to .golangci-fast.yml with critical checks (G101, G110, G305) - Replace broad .golangci.yml exclusions with targeted path-specific rules - Test files now linted on every commit Test Fixes: - Fix emergency route count assertions (1→2 for dual-port setup) - Fix DNS provider service tests with proper mock setup - Fix certificate service tests with deterministic behavior Backend: 27 packages pass, 83.5% coverage Frontend: 0 lint warnings, 0 TypeScript errors Pre-commit: All 14 hooks pass (~37s)
100 lines
4.0 KiB
TypeScript
100 lines
4.0 KiB
TypeScript
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
import ImportSitesModal from './ImportSitesModal'
|
|
import { vi } from 'vitest'
|
|
import { CaddyFile } from '../api/import'
|
|
|
|
// Mock the upload API used by the component
|
|
const mockUpload = vi.fn()
|
|
vi.mock('../api/import', () => ({
|
|
uploadCaddyfilesMulti: (files: CaddyFile[]) => mockUpload(files),
|
|
}))
|
|
|
|
describe('ImportSitesModal', () => {
|
|
beforeEach(() => {
|
|
mockUpload.mockReset()
|
|
})
|
|
|
|
test('renders modal, add and remove sites, and edits textarea', () => {
|
|
const onClose = vi.fn()
|
|
render(<ImportSitesModal visible={true} onClose={onClose} />)
|
|
|
|
// modal container is present
|
|
expect(screen.getByTestId('multi-site-modal')).toBeInTheDocument()
|
|
|
|
// initially one site with filename input and content textarea
|
|
const textareas = screen.getAllByRole('textbox').filter(el => el.tagName === 'TEXTAREA')
|
|
expect(textareas.length).toBe(1)
|
|
|
|
// add a site -> two sites
|
|
fireEvent.click(screen.getByText('+ Add site'))
|
|
const textareasAfterAdd = screen.getAllByRole('textbox').filter(el => el.tagName === 'TEXTAREA')
|
|
expect(textareasAfterAdd.length).toBe(2)
|
|
|
|
// remove the second site (use getAllByText since multiple Remove buttons now exist)
|
|
const removeButtons = screen.getAllByText('Remove')
|
|
fireEvent.click(removeButtons[removeButtons.length - 1])
|
|
const textareasAfterRemove = screen.getAllByRole('textbox').filter(el => el.tagName === 'TEXTAREA')
|
|
expect(textareasAfterRemove.length).toBe(1)
|
|
|
|
// type into textarea
|
|
const ta = screen.getAllByRole('textbox').filter(el => el.tagName === 'TEXTAREA')[0]
|
|
fireEvent.change(ta, { target: { value: 'example.com { reverse_proxy 127.0.0.1:8080 }' } })
|
|
expect((ta as HTMLTextAreaElement).value).toContain('example.com')
|
|
})
|
|
|
|
test('reads multiple files via hidden input and submits successfully', async () => {
|
|
const onClose = vi.fn()
|
|
const onUploaded = vi.fn()
|
|
mockUpload.mockResolvedValueOnce(undefined)
|
|
|
|
const { container } = render(<ImportSitesModal visible={true} onClose={onClose} onUploaded={onUploaded} />)
|
|
|
|
// find the hidden file input
|
|
const input: HTMLInputElement | null = container.querySelector('input[type="file"]')
|
|
expect(input).toBeTruthy()
|
|
|
|
// create two files (note: jsdom's File.text() returns empty strings, so we'll set content manually)
|
|
const f1 = new File(['site1'], 'site1.caddy', { type: 'text/plain' })
|
|
const f2 = new File(['site2'], 'site2.caddy', { type: 'text/plain' })
|
|
|
|
// fire change event with files
|
|
fireEvent.change(input!, { target: { files: [f1, f2] } })
|
|
|
|
// after input, two textareas should appear (one per file)
|
|
await waitFor(() => {
|
|
const textareas = screen.getAllByRole('textbox').filter(el => el.tagName === 'TEXTAREA')
|
|
expect(textareas.length).toBe(2)
|
|
})
|
|
|
|
// Manually fill textareas since jsdom's File.text() doesn't work correctly
|
|
const textareas = screen.getAllByRole('textbox').filter(el => el.tagName === 'TEXTAREA')
|
|
fireEvent.change(textareas[0], { target: { value: 'site1' } })
|
|
fireEvent.change(textareas[1], { target: { value: 'site2' } })
|
|
|
|
// submit
|
|
fireEvent.click(screen.getByText('Parse and Review'))
|
|
|
|
await waitFor(() => expect(mockUpload).toHaveBeenCalled())
|
|
// New API contract: files are passed as {filename, content} objects
|
|
expect(mockUpload).toHaveBeenCalledWith([
|
|
{ filename: 'site1.caddy', content: 'site1' },
|
|
{ filename: 'site2.caddy', content: 'site2' },
|
|
])
|
|
expect(onUploaded).toHaveBeenCalled()
|
|
expect(onClose).toHaveBeenCalled()
|
|
})
|
|
|
|
test('displays error when upload fails', async () => {
|
|
const onClose = vi.fn()
|
|
mockUpload.mockRejectedValueOnce(new Error('upload-failed'))
|
|
|
|
render(<ImportSitesModal visible={true} onClose={onClose} />)
|
|
|
|
// click submit with default empty site
|
|
fireEvent.click(screen.getByText('Parse and Review'))
|
|
|
|
// error message appears
|
|
await waitFor(() => expect(screen.getByText(/upload-failed|Upload failed/i)).toBeInTheDocument())
|
|
})
|
|
})
|