mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(hub,web): harden notification preference updates
This commit is contained in:
@@ -63,6 +63,16 @@ describe('PUT /api/notification-preferences', () => {
|
|||||||
expect(res.status).toBe(400)
|
expect(res.status).toBe(400)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('rejects fractional preference values', async () => {
|
||||||
|
const app = createApp()
|
||||||
|
const res = await app.request('/api/notification-preferences', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ permissionRequests: 0.5 })
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(400)
|
||||||
|
})
|
||||||
|
|
||||||
it('rejects invalid bodies', async () => {
|
it('rejects invalid bodies', async () => {
|
||||||
const app = createApp()
|
const app = createApp()
|
||||||
const res = await app.request('/api/notification-preferences', {
|
const res = await app.request('/api/notification-preferences', {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import type { Store } from '../../store'
|
|||||||
import type { WebAppEnv } from '../middleware/auth'
|
import type { WebAppEnv } from '../middleware/auth'
|
||||||
|
|
||||||
const updateSchema = z.object({
|
const updateSchema = z.object({
|
||||||
permissionRequests: z.number().min(0).max(1).optional(),
|
permissionRequests: z.number().int().min(0).max(1).optional(),
|
||||||
sessionReady: z.number().min(0).max(1).optional(),
|
sessionReady: z.number().int().min(0).max(1).optional(),
|
||||||
taskNotifications: z.number().min(0).max(1).optional(),
|
taskNotifications: z.number().int().min(0).max(1).optional(),
|
||||||
sessionCompletion: z.number().min(0).max(1).optional()
|
sessionCompletion: z.number().int().min(0).max(1).optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
export function createNotificationPreferencesRoutes(store: Store): Hono<WebAppEnv> {
|
export function createNotificationPreferencesRoutes(store: Store): Hono<WebAppEnv> {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { I18nProvider } from '@/lib/i18n-context'
|
import { I18nProvider } from '@/lib/i18n-context'
|
||||||
|
import { queryKeys } from '@/lib/query-keys'
|
||||||
import SettingsNotificationsPage from './notifications'
|
import SettingsNotificationsPage from './notifications'
|
||||||
|
|
||||||
const defaultPrefs = {
|
const defaultPrefs = {
|
||||||
@@ -66,6 +67,7 @@ describe('SettingsNotificationsPage', () => {
|
|||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</QueryClientProvider>,
|
</QueryClientProvider>,
|
||||||
)
|
)
|
||||||
|
return queryClient
|
||||||
}
|
}
|
||||||
|
|
||||||
it('renders all four toggles from server preferences', async () => {
|
it('renders all four toggles from server preferences', async () => {
|
||||||
@@ -105,6 +107,16 @@ describe('SettingsNotificationsPage', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps the confirmation open when disabling permission notifications fails', async () => {
|
||||||
|
updateNotificationPreferences.mockRejectedValueOnce(new Error('save failed'))
|
||||||
|
renderPage()
|
||||||
|
fireEvent.click(await screen.findByLabelText('Permission requests'))
|
||||||
|
fireEvent.click(screen.getByText('Turn off anyway'))
|
||||||
|
|
||||||
|
expect(await screen.findByText('save failed')).toBeTruthy()
|
||||||
|
expect(screen.getByText('Turn off permission request notifications?')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
it('sends a test push and reports success', async () => {
|
it('sends a test push and reports success', async () => {
|
||||||
renderPage()
|
renderPage()
|
||||||
const button = await screen.findByRole('button', { name: 'Send test push' })
|
const button = await screen.findByRole('button', { name: 'Send test push' })
|
||||||
@@ -165,6 +177,29 @@ describe('SettingsNotificationsPage', () => {
|
|||||||
expect(await screen.findByText('Failed to save notification copy')).toBeTruthy()
|
expect(await screen.findByText('Failed to save notification copy')).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('accepts refreshed copy after a successful save', async () => {
|
||||||
|
updateNotificationCopy.mockResolvedValueOnce({
|
||||||
|
...defaultCopyResponse,
|
||||||
|
copy: { ready: { title: 'Saved title', body: 'Saved body' } }
|
||||||
|
})
|
||||||
|
const queryClient = renderPage()
|
||||||
|
const readyRow = await screen.findByRole('button', { name: /Session ready.*Ready for input/ })
|
||||||
|
fireEvent.click(readyRow)
|
||||||
|
fireEvent.change(screen.getByLabelText('Title'), { target: { value: 'Saved title' } })
|
||||||
|
fireEvent.change(screen.getByLabelText('Body'), { target: { value: 'Saved body' } })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Save copy' }))
|
||||||
|
expect(await screen.findByText('Copy saved')).toBeTruthy()
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
queryClient.setQueryData(queryKeys.notificationCopy, {
|
||||||
|
...defaultCopyResponse,
|
||||||
|
copy: { ready: { title: 'Remote title', body: 'Remote body' } }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await screen.findByRole('button', { name: /Session ready.*Remote title.*Remote body/ })).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps copy editors collapsed and prefills defaults when opened', async () => {
|
it('keeps copy editors collapsed and prefills defaults when opened', async () => {
|
||||||
renderPage()
|
renderPage()
|
||||||
const readyRow = await screen.findByRole('button', { name: /Session ready.*Ready for input/ })
|
const readyRow = await screen.findByRole('button', { name: /Session ready.*Ready for input/ })
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ export default function SettingsNotificationsPage() {
|
|||||||
setCopySaveError(null)
|
setCopySaveError(null)
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
userEditedRef.current = false
|
||||||
queryClient.setQueryData(queryKeys.notificationCopy, data)
|
queryClient.setQueryData(queryKeys.notificationCopy, data)
|
||||||
setDraft(resolveEffectiveCopy(data.copy, data.defaults))
|
setDraft(resolveEffectiveCopy(data.copy, data.defaults))
|
||||||
setCopySaveError(null)
|
setCopySaveError(null)
|
||||||
@@ -336,7 +337,7 @@ export default function SettingsNotificationsPage() {
|
|||||||
confirmLabel={t('settings.notifications.disablePermissionConfirm')}
|
confirmLabel={t('settings.notifications.disablePermissionConfirm')}
|
||||||
confirmingLabel={t('settings.notifications.disablePermissionConfirm')}
|
confirmingLabel={t('settings.notifications.disablePermissionConfirm')}
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
mutation.mutate({ permissionRequests: 0 })
|
await mutation.mutateAsync({ permissionRequests: 0 })
|
||||||
}}
|
}}
|
||||||
isPending={mutation.isPending}
|
isPending={mutation.isPending}
|
||||||
destructive
|
destructive
|
||||||
|
|||||||
Reference in New Issue
Block a user