test(caddy): cover invalid path branches; ci: handle go test non-zero when coverage file exists

This commit is contained in:
CI
2025-11-29 08:55:25 +00:00
parent 0c62118989
commit fcc273262c
51 changed files with 1117 additions and 205 deletions
+6 -1
View File
@@ -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>
)
}
+54 -19
View File
@@ -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>
);
};
+3 -2
View File
@@ -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);
});
});
+7 -4
View File
@@ -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()