WP-02: contracts and failing tests for settings, mail, jobs and scheduler
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
41
frontend/e2e/settings-jobs.spec.ts
Normal file
41
frontend/e2e/settings-jobs.spec.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { test, expect, type Page } from '@playwright/test'
|
||||
|
||||
async function loginAdmin(page: Page) {
|
||||
await page.goto('/login')
|
||||
await page.getByLabel('Email').fill('admin@example.com')
|
||||
await page.getByLabel('Password').fill('admin-password-123')
|
||||
await page.getByRole('button', { name: 'Sign in' }).click()
|
||||
await expect(page).toHaveURL('/')
|
||||
}
|
||||
|
||||
test('admin saves SMTP settings and a schedule', async ({ page }) => {
|
||||
await loginAdmin(page)
|
||||
await page.getByRole('link', { name: 'Settings' }).click()
|
||||
await page.getByLabel('SMTP host').fill('mail.example.com')
|
||||
await page.getByLabel('Port').fill('587')
|
||||
await page.getByLabel('Username').fill('bot')
|
||||
await page.getByLabel('Password').fill('secret-password')
|
||||
await page.getByLabel('From address').fill('monitoring@example.com')
|
||||
await page.getByLabel('Notification recipients').fill('ops@example.com')
|
||||
await page.getByRole('button', { name: 'Save SMTP settings' }).click()
|
||||
await expect(page.getByRole('status')).toContainText('SMTP settings saved')
|
||||
await page.reload()
|
||||
await expect(page.getByLabel('SMTP host')).toHaveValue('mail.example.com')
|
||||
await expect(page.getByText('leave empty to keep')).toBeVisible()
|
||||
|
||||
const cron = page.getByLabel('Package refresh schedule')
|
||||
await cron.fill('0 */30 * * * *')
|
||||
await page.getByRole('button', { name: 'Save schedules' }).click()
|
||||
await expect(page.getByRole('status')).toContainText('Schedules saved')
|
||||
})
|
||||
|
||||
test('admin runs a job manually and sees the log', async ({ page }) => {
|
||||
await loginAdmin(page)
|
||||
await page.getByRole('link', { name: 'Jobs' }).click()
|
||||
await page.getByLabel('Job').selectOption('package_refresh')
|
||||
await page.getByRole('button', { name: 'Run now' }).click()
|
||||
const row = page.getByRole('row', { name: /package_refresh/ }).first()
|
||||
await expect(row).toContainText(/success|running/)
|
||||
await row.getByRole('button', { name: 'Log' }).click()
|
||||
await expect(page.getByTestId('job-log')).not.toBeEmpty()
|
||||
})
|
||||
33
frontend/src/components/SmtpForm.test.ts
Normal file
33
frontend/src/components/SmtpForm.test.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import SmtpForm from './SmtpForm.vue'
|
||||
|
||||
const current = {
|
||||
host: 'mail.example.com',
|
||||
port: 587,
|
||||
security: 'starttls' as const,
|
||||
username: 'bot',
|
||||
from: 'mon@example.com',
|
||||
notify_to: ['ops@example.com', 'dev@example.com'],
|
||||
password_set: true,
|
||||
}
|
||||
|
||||
describe('SmtpForm', () => {
|
||||
it('prefills current settings and emits the payload with recipients as a list', async () => {
|
||||
const w = mount(SmtpForm, { props: { current } })
|
||||
expect((w.find('input[name=host]').element as HTMLInputElement).value).toBe('mail.example.com')
|
||||
expect((w.find('input[name=notify_to]').element as HTMLInputElement).value).toBe(
|
||||
'ops@example.com, dev@example.com',
|
||||
)
|
||||
await w.find('input[name=notify_to]').setValue('a@x.de; b@x.de')
|
||||
await w.find('form').trigger('submit')
|
||||
const payload = w.emitted('submit')![0][0] as Record<string, unknown>
|
||||
expect(payload.notify_to).toEqual(['a@x.de', 'b@x.de'])
|
||||
expect(payload.password).toBe('')
|
||||
expect(payload.port).toBe(587)
|
||||
})
|
||||
|
||||
it('shows a hint that a password is stored', () => {
|
||||
const w = mount(SmtpForm, { props: { current } })
|
||||
expect(w.text()).toContain('leave empty to keep')
|
||||
})
|
||||
})
|
||||
52
frontend/src/pages/JobsPage.test.ts
Normal file
52
frontend/src/pages/JobsPage.test.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import JobsPage from './JobsPage.vue'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { api } from '../api/client'
|
||||
|
||||
vi.mock('../api/client', () => ({
|
||||
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn() },
|
||||
ApiError: class extends Error {},
|
||||
}))
|
||||
|
||||
const run = {
|
||||
id: 'r1',
|
||||
kind: 'package_refresh',
|
||||
status: 'success',
|
||||
started_at: '2026-09-02T10:00:00Z',
|
||||
finished_at: '2026-09-02T10:00:05Z',
|
||||
log: 'hello\n',
|
||||
triggered_by: 'admin@example.com',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.mocked(api.get).mockImplementation(async (path: string) => {
|
||||
if (path.startsWith('/api/jobs/kinds')) return ['package_refresh']
|
||||
return [run]
|
||||
})
|
||||
})
|
||||
|
||||
describe('JobsPage', () => {
|
||||
it('lists runs and lets an admin run a job', async () => {
|
||||
const auth = useAuthStore()
|
||||
auth.user = { id: '1', email: 'a', display_name: 'A', role: 'admin', is_active: true }
|
||||
vi.mocked(api.post).mockResolvedValue({ ...run, id: 'r2', status: 'running' })
|
||||
const w = mount(JobsPage)
|
||||
await flushPromises()
|
||||
expect(w.text()).toContain('package_refresh')
|
||||
expect(w.text()).toContain('success')
|
||||
await w.find('select[name=kind]').setValue('package_refresh')
|
||||
await w.find('button[name=run]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(api.post).toHaveBeenCalledWith('/api/jobs/run', { kind: 'package_refresh' })
|
||||
})
|
||||
|
||||
it('hides the run control for non-admins', async () => {
|
||||
const auth = useAuthStore()
|
||||
auth.user = { id: '1', email: 'a', display_name: 'A', role: 'user', is_active: true }
|
||||
const w = mount(JobsPage)
|
||||
await flushPromises()
|
||||
expect(w.find('button[name=run]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user