fix: sanitize API error text display and add time_blockers tests

Add extractErrorMessage helper for JSON error body parsing and apply sanitizeText across all toast displays. Add time_blockers test coverage for new holiday placeholder cleanup and overlapping scenarios.
This commit is contained in:
2026-08-22 00:34:48 +01:00
parent 388ac5948a
commit 3eec71a56c
30 changed files with 283 additions and 71 deletions
@@ -2,7 +2,7 @@
import { SvelteDate } from 'svelte/reactivity';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { sanitizeText } from '$lib/utils/toast-safe';
import { extractErrorMessage, sanitizeText } from '$lib/utils/toast-safe';
import { formatDateTime, formatDuration } from '$lib/utils/format';
import { formatUserName } from '$lib/utils/nameDisplay';
import { parseWallClockDate } from '$lib/utils/timeSlots';
@@ -335,7 +335,7 @@
onApproved();
} else {
const text = await response.text();
toast.error('Failed to confirm: ' + sanitizeText(text), { id: loadingToast });
toast.error('Failed to confirm: ' + sanitizeText(extractErrorMessage(text)), { id: loadingToast });
}
} catch {
toast.error('Network error confirming booking', { id: loadingToast });
@@ -363,7 +363,7 @@
onApproved();
} else {
const text = await response.text();
toast.error('Failed to decline: ' + sanitizeText(text), { id: loadingToast });
toast.error('Failed to decline: ' + sanitizeText(extractErrorMessage(text)), { id: loadingToast });
}
} catch {
toast.error('Network error declining booking', { id: loadingToast });
@@ -1,6 +1,7 @@
<script lang="ts">
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { SvelteDate, SvelteSet, SvelteURLSearchParams } from 'svelte/reactivity';
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
import { isValidUKPhone, toE164UK } from '$lib/utils/phone';
@@ -718,7 +719,7 @@
return false;
} else {
const errorText = await response.text();
toast.error(`Failed to reserve slot: ${errorText}`);
toast.error(`Failed to reserve slot: ${extractErrorMessage(errorText)}`);
return false;
}
} catch {
@@ -855,7 +856,7 @@
toast.success('Custom service created and added');
} else {
const err = await response.text();
toast.error(`Failed: ${err}`);
toast.error(`Failed: ${extractErrorMessage(err)}`);
}
} catch {
toast.error('Network error');
@@ -1014,7 +1015,7 @@
onBookingCreated?.();
} else {
const errorText = await res.text();
toast.error(`Failed to create booking: ${errorText}`);
toast.error(`Failed to create booking: ${extractErrorMessage(errorText)}`);
}
} catch {
toast.error('An error occurred while creating booking');
@@ -2,6 +2,7 @@
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { SvelteDate } from 'svelte/reactivity';
import * as Modal from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
@@ -84,7 +85,7 @@
fetchBookingDetails();
} else {
const text = await response.text();
toast.error('Failed to cancel: ' + text);
toast.error('Failed to cancel: ' + extractErrorMessage(text));
}
} catch {
toast.error('Network error');
@@ -210,7 +211,7 @@
};
} else {
const text = await response.text();
toast.error('Failed to load booking details: ' + text);
toast.error('Failed to load booking details: ' + extractErrorMessage(text));
}
} catch (_err) {
console.error('Network error loading booking details:', _err);
@@ -2,6 +2,7 @@
import { apiFetch } from '$lib/utils/api';
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
// shadcn-svelte components
import { Button } from '$lib/components/ui/button';
@@ -77,7 +78,7 @@
}
} else {
const text = await response.text();
toast.error('Failed to load bookings: ' + text);
toast.error('Failed to load bookings: ' + extractErrorMessage(text));
}
} catch (err) {
console.error('Error fetching bookings:', err);
@@ -2,6 +2,7 @@
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
@@ -257,7 +258,7 @@
showEditModal = false;
} else {
const errText = await res.text();
toast.error(errText || 'Failed to update settings');
toast.error(extractErrorMessage(errText) || 'Failed to update settings');
}
} catch {
toast.error('Network error saving settings');
@@ -1,7 +1,7 @@
<script lang="ts">
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { sanitizeText } from '$lib/utils/toast-safe';
import { extractErrorMessage, sanitizeText } from '$lib/utils/toast-safe';
import { range } from '$lib/utils/format';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
@@ -156,7 +156,7 @@
await fetchServices();
} else {
const err = await response.text();
toast.error('Failed: ' + sanitizeText(err));
toast.error('Failed: ' + sanitizeText(extractErrorMessage(err)));
}
} catch {
toast.error('Network error');
@@ -184,7 +184,7 @@
toast.error('A service with this name already exists');
} else {
const errText = await response.text();
toast.error('Failed: ' + sanitizeText(errText));
toast.error('Failed: ' + sanitizeText(extractErrorMessage(errText)));
}
} catch {
toast.error('Network error');
@@ -204,7 +204,7 @@
toast.error('Cannot delete: used in bookings. Promote first.');
} else {
const errText = await response.text();
toast.error('Failed: ' + sanitizeText(errText));
toast.error('Failed: ' + sanitizeText(extractErrorMessage(errText)));
}
} catch {
toast.error('Network error');
@@ -3,6 +3,7 @@
import { apiFetch } from '$lib/utils/api';
import { range } from '$lib/utils/format';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
@@ -186,7 +187,7 @@
await loadCampaigns();
} else {
const text = await res.text();
toast.error(text || 'Failed to save campaign');
toast.error(extractErrorMessage(text) || 'Failed to save campaign');
}
} catch {
toast.error('Failed to save campaign');
@@ -2,6 +2,7 @@
import { apiFetch } from '$lib/utils/api';
import { SvelteDate } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { formatDuration } from '$lib/utils/format';
import { formatUserName } from '$lib/utils/nameDisplay';
import { parseWallClockDate } from '$lib/utils/timeSlots';
@@ -138,7 +139,7 @@
notes = booking.notes || '';
} else {
const text = await response.text();
toast.error('Failed to load booking: ' + text);
toast.error('Failed to load booking: ' + extractErrorMessage(text));
}
} catch (err) {
console.error('Error fetching booking:', err);
@@ -331,7 +332,7 @@
onSaved?.();
} else {
const text = await response.text();
toast.error('Failed to update booking: ' + text);
toast.error('Failed to update booking: ' + extractErrorMessage(text));
}
} catch (err) {
console.error('Error saving booking:', err);
@@ -379,7 +380,7 @@
fetchBooking();
} else {
const text = await response.text();
toast.error('Failed to process refund: ' + text);
toast.error('Failed to process refund: ' + extractErrorMessage(text));
}
} catch (err) {
console.error('Error processing refund:', err);
@@ -1,6 +1,7 @@
<script lang="ts">
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import * as Modal from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
@@ -128,7 +129,7 @@
onApproved();
} else {
const text = await response.text();
toast.error('Failed to approve: ' + text, { id: loadingToast });
toast.error('Failed to approve: ' + extractErrorMessage(text), { id: loadingToast });
}
} catch (err) {
console.error('Error approving edit request:', err);
@@ -160,7 +161,7 @@
onDenied();
} else {
const text = await response.text();
toast.error('Failed to deny: ' + text, { id: loadingToast });
toast.error('Failed to deny: ' + extractErrorMessage(text), { id: loadingToast });
}
} catch (err) {
console.error('Error denying edit request:', err);
@@ -2,6 +2,7 @@
import { authStore } from '$lib/stores/auth.svelte';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
@@ -397,7 +398,7 @@
await fetchGiftCards();
} else {
const errText = await res.text();
toast.error(errText || 'Failed to create inventory card');
toast.error(extractErrorMessage(errText) || 'Failed to create inventory card');
}
} catch {
toast.error('Network error');
@@ -439,7 +440,7 @@
await fetchGiftCards();
} else {
const errText = await res.text();
toast.error(errText || 'Failed to transfer balance');
toast.error(extractErrorMessage(errText) || 'Failed to transfer balance');
}
} catch {
toast.error('Network error transferring balance');
@@ -1,5 +1,6 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import * as Modal from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { apiFetch } from '$lib/utils/api';
@@ -41,7 +42,7 @@
}
} else {
const text = await response.text();
toast.error('Failed to load services: ' + text);
toast.error('Failed to load services: ' + extractErrorMessage(text));
}
} catch (err) {
console.error('Error fetching eligible services:', err);
@@ -77,7 +78,7 @@
onPatchTestAdded();
} else {
const text = await response.text();
toast.error(text || 'Failed to add patch test', { id: loadingToast });
toast.error(extractErrorMessage(text) || 'Failed to add patch test', { id: loadingToast });
}
} catch (err) {
console.error('Error adding patch test:', err);
@@ -1,5 +1,6 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { Button } from '$lib/components/ui/button';
import { apiFetch } from '$lib/utils/api';
import * as Card from '$lib/components/ui/card';
@@ -216,7 +217,7 @@
await fetchData();
} else {
const errText = await res.text();
toast.error(`Failed to save: ${errText}`);
toast.error(`Failed to save: ${extractErrorMessage(errText)}`);
}
} catch (err) {
console.error(err);
@@ -1,6 +1,7 @@
<script lang="ts">
import { SvelteDate, SvelteSet } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { apiFetch } from '$lib/utils/api';
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
import { Checkbox } from '$lib/components/ui/checkbox';
@@ -323,7 +324,7 @@
onSaved?.();
} else {
const text = await response.text();
toast.error('Failed to reschedule: ' + text, { id: loadingToast });
toast.error('Failed to reschedule: ' + extractErrorMessage(text), { id: loadingToast });
}
} catch (err) {
console.error('Error rescheduling booking:', err);
@@ -1,5 +1,6 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { apiFetch } from '$lib/utils/api';
import { range } from '$lib/utils/format';
@@ -192,7 +193,7 @@
await fetchServices();
} else {
const errorText = await response.text();
toast.error(`Failed to update service: ${errorText}`);
toast.error(`Failed to update service: ${extractErrorMessage(errorText)}`);
}
} catch (err) {
console.error('Error toggling service:', err);
@@ -219,7 +220,7 @@
await fetchServices();
} else {
const errorText = await response.text();
toast.error(`Failed to delete service: ${errorText}`);
toast.error(`Failed to delete service: ${extractErrorMessage(errorText)}`);
}
} catch (err) {
console.error('Error deleting service:', err);
@@ -275,10 +276,10 @@
toast.error('A service with this name already exists', { id: loadingToast });
} else if (response.status === 400) {
const errorText = await response.text();
toast.error(`Validation error: ${errorText}`, { id: loadingToast });
toast.error(`Validation error: ${extractErrorMessage(errorText)}`, { id: loadingToast });
} else {
const errorText = await response.text();
toast.error(`Failed to create service: ${errorText}`, { id: loadingToast });
toast.error(`Failed to create service: ${extractErrorMessage(errorText)}`, { id: loadingToast });
}
} catch (err) {
console.error('Error creating service:', err);
@@ -1,6 +1,7 @@
<script lang="ts">
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { apiFetch } from '$lib/utils/api';
import * as Modal from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
@@ -115,7 +116,7 @@
selectedUser = await response.json();
} else {
const text = await response.text();
toast.error('Failed to load user details: ' + text);
toast.error('Failed to load user details: ' + extractErrorMessage(text));
}
} catch (err) {
console.error('Error fetching user details:', err);
@@ -191,7 +192,7 @@
}
} else {
const text = await response.text();
toast.error('Failed to load user bookings: ' + text);
toast.error('Failed to load user bookings: ' + extractErrorMessage(text));
}
} catch (err) {
console.error('Error fetching user bookings:', err);
@@ -1,5 +1,6 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { apiFetch } from '$lib/utils/api';
import { range } from '$lib/utils/format';
import * as Card from '$lib/components/ui/card';
@@ -94,7 +95,7 @@
}
} else {
const text = await response.text();
toast.error('Failed to load users: ' + text);
toast.error('Failed to load users: ' + extractErrorMessage(text));
}
} catch (err) {
console.error('Error fetching users:', err);
@@ -6,6 +6,7 @@
import { SvelteDate } from 'svelte/reactivity';
import { onMount, onDestroy } from 'svelte';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import type { AvailableHoursDay, Service } from '$lib/types/booking';
import {
@@ -306,7 +307,7 @@
return false;
} else {
const errorText = await response.text();
toast.error(`Failed to reserve slot: ${errorText}`);
toast.error(`Failed to reserve slot: ${extractErrorMessage(errorText)}`);
return false;
}
} catch (_err) {
@@ -2,6 +2,7 @@
import { generateUUID } from '$lib/utils/uuid';
import { apiFetch } from '$lib/utils/api';
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { SvelteDate, SvelteURLSearchParams } from 'svelte/reactivity';
import { isValidUKPhone, toE164UK } from '$lib/utils/phone';
import { formatUserName } from '$lib/utils/nameDisplay';
@@ -409,7 +410,7 @@
toast.success('Custom service created and added');
} else {
const err = await response.text();
toast.error(`Failed: ${err}`);
toast.error(`Failed: ${extractErrorMessage(err)}`);
}
} catch {
toast.error('Network error');
@@ -555,7 +556,7 @@
onBookingCreated?.();
} else {
const errorText = await res.text();
toast.error(`Failed to create booking: ${errorText}`);
toast.error(`Failed to create booking: ${extractErrorMessage(errorText)}`);
}
} catch {
toast.error('An error occurred while creating booking');
@@ -1,5 +1,6 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { extractErrorMessage } from '$lib/utils/toast-safe';
import { browser } from '$app/environment';
import { apiFetch } from '$lib/utils/api';
import { range } from '$lib/utils/format';
@@ -128,7 +129,7 @@
);
} else {
const text = await response.text();
error = 'Failed to load working hours: ' + text;
error = 'Failed to load working hours: ' + extractErrorMessage(text);
console.error('Error fetching default hours:', text);
}
} catch (err) {
@@ -185,7 +186,7 @@
toast.error('Unauthorized. Please log in again.', { id: loadingToast });
} else {
const text = await response.text();
toast.error('Failed to save: ' + text, { id: loadingToast });
toast.error('Failed to save: ' + extractErrorMessage(text), { id: loadingToast });
}
} catch (err) {
console.error('save default hours', err);