feat(notification): add dismiss-notification and mark-all-read form actions to aktivitaeten
Adds two SvelteKit form actions to /aktivitaeten/+page.server.ts so the notification bell can POST there instead of calling the backend directly from the browser. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { createApiClient } from '$lib/shared/api.server';
|
||||
import { getErrorMessage } from '$lib/shared/errors';
|
||||
import type { components, operations } from '$lib/generated/api';
|
||||
|
||||
type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO'];
|
||||
@@ -65,3 +67,29 @@ export async function load({ fetch, url }) {
|
||||
loadError
|
||||
};
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
'dismiss-notification': async ({ request, fetch }) => {
|
||||
const data = await request.formData();
|
||||
const notificationId = data.get('notificationId') as string;
|
||||
const api = createApiClient(fetch);
|
||||
const result = await api.PATCH('/api/notifications/{id}/read', {
|
||||
params: { path: { id: notificationId } }
|
||||
});
|
||||
if (!result.response.ok) {
|
||||
const code = (result.error as unknown as { code?: string })?.code;
|
||||
return fail(result.response.status, { error: getErrorMessage(code) });
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
'mark-all-read': async ({ fetch }) => {
|
||||
const api = createApiClient(fetch);
|
||||
const result = await api.POST('/api/notifications/read-all');
|
||||
if (!result.response.ok) {
|
||||
const code = (result.error as unknown as { code?: string })?.code;
|
||||
return fail(result.response.status, { error: getErrorMessage(code) });
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { load } from './+page.server';
|
||||
import { load, actions } from './+page.server';
|
||||
|
||||
const mockApi = {
|
||||
GET: vi.fn()
|
||||
GET: vi.fn(),
|
||||
PATCH: vi.fn(),
|
||||
POST: vi.fn()
|
||||
};
|
||||
|
||||
vi.mock('$lib/shared/api.server', () => ({
|
||||
@@ -173,3 +175,77 @@ describe('aktivitaeten/load — kinds param per filter', () => {
|
||||
expect(call[1].params.query.kinds).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function makeActionEvent(formData: FormData): any {
|
||||
return {
|
||||
request: new Request('http://localhost/aktivitaeten', { method: 'POST', body: formData }),
|
||||
fetch
|
||||
};
|
||||
}
|
||||
|
||||
describe('aktivitaeten/actions — dismiss-notification', () => {
|
||||
it('calls PATCH /api/notifications/{id}/read with the form-supplied notificationId', async () => {
|
||||
mockApi.PATCH.mockResolvedValue({ response: { ok: true }, data: {} });
|
||||
const fd = new FormData();
|
||||
fd.set('notificationId', 'n-abc');
|
||||
|
||||
await actions['dismiss-notification'](makeActionEvent(fd));
|
||||
|
||||
expect(mockApi.PATCH).toHaveBeenCalledWith('/api/notifications/{id}/read', {
|
||||
params: { path: { id: 'n-abc' } }
|
||||
});
|
||||
});
|
||||
|
||||
it('returns { success: true } when the API responds ok', async () => {
|
||||
mockApi.PATCH.mockResolvedValue({ response: { ok: true }, data: {} });
|
||||
const fd = new FormData();
|
||||
fd.set('notificationId', 'n-abc');
|
||||
|
||||
const result = await actions['dismiss-notification'](makeActionEvent(fd));
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('returns fail(status, { error }) when the API responds non-ok', async () => {
|
||||
mockApi.PATCH.mockResolvedValue({
|
||||
response: { ok: false, status: 403 },
|
||||
error: { code: 'NOTIFICATION_NOT_FOUND' }
|
||||
});
|
||||
const fd = new FormData();
|
||||
fd.set('notificationId', 'n-abc');
|
||||
|
||||
const result = await actions['dismiss-notification'](makeActionEvent(fd));
|
||||
|
||||
expect(result).toMatchObject({ status: 403 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('aktivitaeten/actions — mark-all-read', () => {
|
||||
it('calls POST /api/notifications/read-all', async () => {
|
||||
mockApi.POST.mockResolvedValue({ response: { ok: true }, data: null });
|
||||
|
||||
await actions['mark-all-read'](makeActionEvent(new FormData()));
|
||||
|
||||
expect(mockApi.POST).toHaveBeenCalledWith('/api/notifications/read-all');
|
||||
});
|
||||
|
||||
it('returns { success: true } when the API responds ok', async () => {
|
||||
mockApi.POST.mockResolvedValue({ response: { ok: true }, data: null });
|
||||
|
||||
const result = await actions['mark-all-read'](makeActionEvent(new FormData()));
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('returns fail(status, { error }) when the API responds non-ok', async () => {
|
||||
mockApi.POST.mockResolvedValue({
|
||||
response: { ok: false, status: 500 },
|
||||
error: { code: 'INTERNAL_ERROR' }
|
||||
});
|
||||
|
||||
const result = await actions['mark-all-read'](makeActionEvent(new FormData()));
|
||||
|
||||
expect(result).toMatchObject({ status: 500 });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user