test(caddy): cover invalid path branches; ci: handle go test non-zero when coverage file exists
This commit is contained in:
@@ -58,7 +58,11 @@ export default function Login() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-dark-bg flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md" title="Login">
|
||||
<div className="w-full max-w-md space-y-4">
|
||||
<div className="flex items-center justify-center">
|
||||
<img src="/logo.png" alt="Charon" className="h-12 w-auto" />
|
||||
</div>
|
||||
<Card className="w-full" title="Login">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<Input
|
||||
label="Email"
|
||||
@@ -103,6 +107,7 @@ export default function Login() {
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getProviders, createProvider, updateProvider, deleteProvider, testProvider, NotificationProvider } from '../api/notifications';
|
||||
import { getProviders, createProvider, updateProvider, deleteProvider, testProvider, getTemplates, previewProvider, NotificationProvider } from '../api/notifications';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Bell, Plus, Trash2, Edit2, Send, Check, X, Loader2 } from 'lucide-react';
|
||||
@@ -16,6 +16,7 @@ const ProviderForm: React.FC<{
|
||||
type: 'discord',
|
||||
enabled: true,
|
||||
config: '',
|
||||
template: 'minimal',
|
||||
notify_proxy_hosts: true,
|
||||
notify_remote_servers: true,
|
||||
notify_domains: true,
|
||||
@@ -25,6 +26,8 @@ const ProviderForm: React.FC<{
|
||||
});
|
||||
|
||||
const [testStatus, setTestStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
const [previewContent, setPreviewContent] = useState<string | null>(null);
|
||||
const [previewError, setPreviewError] = useState<string | null>(null);
|
||||
|
||||
const testMutation = useMutation({
|
||||
mutationFn: testProvider,
|
||||
@@ -43,10 +46,26 @@ const ProviderForm: React.FC<{
|
||||
testMutation.mutate(formData as Partial<NotificationProvider>);
|
||||
};
|
||||
|
||||
const type = watch('type');
|
||||
const handlePreview = async () => {
|
||||
const formData = watch();
|
||||
setPreviewContent(null);
|
||||
setPreviewError(null);
|
||||
try {
|
||||
const res = await previewProvider(formData as Partial<NotificationProvider>);
|
||||
if (res.parsed) setPreviewContent(JSON.stringify(res.parsed, null, 2)); else setPreviewContent(res.rendered);
|
||||
} catch (err: any) {
|
||||
setPreviewError(err?.response?.data?.error || err?.message || 'Failed to generate preview');
|
||||
}
|
||||
};
|
||||
|
||||
const setTemplate = (template: string) => {
|
||||
setValue('config', template);
|
||||
const type = watch('type');
|
||||
const { data: templatesList } = useQuery({ queryKey: ['notificationTemplates'], queryFn: getTemplates });
|
||||
const template = watch('template');
|
||||
|
||||
const setTemplate = (templateStr: string, templateName?: string) => {
|
||||
// If templateName is provided, set template selection as well
|
||||
if (templateName) setValue('template', templateName);
|
||||
setValue('config', templateStr);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -93,23 +112,23 @@ const ProviderForm: React.FC<{
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">JSON Payload Template</label>
|
||||
<div className="flex gap-2 mb-2 mt-1">
|
||||
<Button type="button" size="sm" variant="secondary" onClick={() => setTemplate('{"content": "{{.Title}}: {{.Message}}"}')}>
|
||||
Simple Template
|
||||
<Button type="button" size="sm" variant={template === 'minimal' ? 'primary' : 'secondary'} onClick={() => setTemplate('{"message": "{{.Message}}", "title": "{{.Title}}", "time": "{{.Time}}", "event": "{{.EventType}}"}', 'minimal')}>
|
||||
Minimal Template
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant="secondary" onClick={() => setTemplate(`{
|
||||
"embeds": [{
|
||||
"title": "{{.Title}}",
|
||||
"description": "{{.Message}}",
|
||||
"color": 15158332,
|
||||
"fields": [
|
||||
{ "name": "Monitor", "value": "{{.Name}}", "inline": true },
|
||||
{ "name": "Status", "value": "{{.Status}}", "inline": true },
|
||||
{ "name": "Latency", "value": "{{.Latency}}ms", "inline": true }
|
||||
]
|
||||
}]
|
||||
}`)}>
|
||||
Detailed Template (Discord)
|
||||
<Button type="button" size="sm" variant={template === 'detailed' ? 'primary' : 'secondary'} onClick={() => setTemplate(`{"title": "{{.Title}}", "message": "{{.Message}}", "time": "{{.Time}}", "event": "{{.EventType}}", "host": "{{.HostName}}", "host_ip": "{{.HostIP}}", "service_count": {{.ServiceCount}}, "services": {{.Services}}}`, 'detailed')}>
|
||||
Detailed Template
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant={template === 'custom' ? 'primary' : 'secondary'} onClick={() => setValue('template', 'custom')}>
|
||||
Custom
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Template</label>
|
||||
<select {...register('template')} className="mt-1 block w-full rounded-md border-gray-300">
|
||||
{templatesList?.map((t: any) => (
|
||||
<option key={t.id} value={t.id}>{t.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<textarea
|
||||
{...register('config')}
|
||||
@@ -160,6 +179,15 @@ const ProviderForm: React.FC<{
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button variant="secondary" onClick={onClose}>Cancel</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handlePreview}
|
||||
disabled={testMutation.isPending}
|
||||
className="min-w-[80px]"
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
@@ -174,6 +202,13 @@ const ProviderForm: React.FC<{
|
||||
</Button>
|
||||
<Button type="submit">Save</Button>
|
||||
</div>
|
||||
{previewError && <div className="mt-2 text-sm text-red-600">Preview Error: {previewError}</div>}
|
||||
{previewContent && (
|
||||
<div className="mt-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Preview Result</label>
|
||||
<pre className="mt-1 p-2 bg-gray-50 dark:bg-gray-800 rounded text-xs overflow-auto whitespace-pre-wrap">{previewContent}</pre>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -91,9 +91,10 @@ const Setup: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8 bg-white dark:bg-gray-800 p-8 rounded-lg shadow-md">
|
||||
<div>
|
||||
<div className="flex flex-col items-center">
|
||||
<img src="/logo.png" alt="Charon" className="h-12 w-auto mb-4" />
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||
Welcome to CPM+
|
||||
Welcome to Charon
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
Create your administrator account to get started.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import Login from '../Login';
|
||||
import * as setupApi from '../../api/setup';
|
||||
|
||||
// Mock AuthContext so useAuth works in tests
|
||||
vi.mock('../../hooks/useAuth', () => ({
|
||||
useAuth: () => ({
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
user: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock API client
|
||||
vi.mock('../../api/client', () => ({
|
||||
default: {
|
||||
post: vi.fn().mockResolvedValue({ data: {} }),
|
||||
get: vi.fn().mockResolvedValue({ data: {} }),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock react-router-dom
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockNavigate,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the API module
|
||||
vi.mock('../../api/setup', () => ({
|
||||
getSetupStatus: vi.fn(),
|
||||
performSetup: vi.fn(),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const renderWithProviders = (ui: React.ReactNode) => {
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
{ui}
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Login Page', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
it('renders login form and logo when setup is not required', async () => {
|
||||
vi.mocked(setupApi.getSetupStatus).mockResolvedValue({ setupRequired: false });
|
||||
|
||||
renderWithProviders(<Login />);
|
||||
|
||||
// The page will redirect to setup if setup is required; for our test we mock it as not required
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Sign In' })).toBeTruthy();
|
||||
});
|
||||
|
||||
// Verify logo is present
|
||||
expect(screen.getAllByAltText('Charon').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -71,9 +71,12 @@ describe('Setup Page', () => {
|
||||
renderWithProviders(<Setup />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Welcome to CPM+')).toBeTruthy();
|
||||
expect(screen.getByText('Welcome to Charon')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Verify logo is present
|
||||
expect(screen.getAllByAltText('Charon').length).toBeGreaterThan(0);
|
||||
|
||||
expect(screen.getByLabelText('Name')).toBeTruthy();
|
||||
expect(screen.getByLabelText('Email Address')).toBeTruthy();
|
||||
expect(screen.getByLabelText('Password')).toBeTruthy();
|
||||
@@ -85,7 +88,7 @@ describe('Setup Page', () => {
|
||||
renderWithProviders(<Setup />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Welcome to CPM+')).toBeNull();
|
||||
expect(screen.queryByText('Welcome to Charon')).toBeNull();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -100,7 +103,7 @@ describe('Setup Page', () => {
|
||||
renderWithProviders(<Setup />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Welcome to CPM+')).toBeTruthy();
|
||||
expect(screen.getByText('Welcome to Charon')).toBeTruthy();
|
||||
});
|
||||
|
||||
const user = userEvent.setup()
|
||||
@@ -131,7 +134,7 @@ describe('Setup Page', () => {
|
||||
renderWithProviders(<Setup />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Welcome to CPM+')).toBeTruthy();
|
||||
expect(screen.getByText('Welcome to Charon')).toBeTruthy();
|
||||
});
|
||||
|
||||
const user = userEvent.setup()
|
||||
|
||||
Reference in New Issue
Block a user