caldav 2026.08.27.03 — filters on calendar and contacts
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { buildHonoApp } from './hono';
|
||||
import { CustomError } from './custom-errors';
|
||||
|
||||
/*
|
||||
The property: every error the Hono app renders is JSON — `{ error: string }`, or the object a
|
||||
CustomError's returnValue supplied. Success responses were always JSON; a text/plain failure was the
|
||||
one response every client had to special-case, and the one that broke `| jq`.
|
||||
|
||||
The app under test is the REAL one: `buildHonoApp` is pure, and Hono builds its matcher lazily on the
|
||||
first fetch, so a throwing route added here reaches the same `onError` production traffic does.
|
||||
*/
|
||||
|
||||
const appThatThrows = (error: Error) => {
|
||||
const app = buildHonoApp();
|
||||
app.get('/boom', () => {
|
||||
throw error;
|
||||
});
|
||||
return app;
|
||||
};
|
||||
|
||||
const request = (app: ReturnType<typeof appThatThrows>) => app.request('/boom');
|
||||
|
||||
describe('onError renders JSON', () => {
|
||||
test('a CustomError with a message → { error: message }, its own status', async () => {
|
||||
const res = await request(appThatThrows(new CustomError(404, 'no such playlist')));
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.headers.get('content-type')).toContain('application/json');
|
||||
expect(await res.json()).toEqual({ error: 'no such playlist' });
|
||||
});
|
||||
|
||||
// An object returnValue passes through UNWRAPPED — callers read its fields (TOO_MANY_REQUESTS
|
||||
// carries `retryAfter`), so `{ error: {...} }` would break them.
|
||||
test('a CustomError with an object returnValue → that object, untouched', async () => {
|
||||
const res = await request(appThatThrows(new CustomError(429, 'Too many requests', { retryAfter: 30 })));
|
||||
expect(res.status).toBe(429);
|
||||
expect(res.headers.get('content-type')).toContain('application/json');
|
||||
expect(await res.json()).toEqual({ retryAfter: 30 });
|
||||
});
|
||||
|
||||
// A string returnValue was the text/plain case before 2026-08-25; it takes the same shape as a message.
|
||||
test('a CustomError with a string returnValue → { error: returnValue }', async () => {
|
||||
const res = await request(appThatThrows(new CustomError(400, 'ignored', 'the field is required')));
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.headers.get('content-type')).toContain('application/json');
|
||||
expect(await res.json()).toEqual({ error: 'the field is required' });
|
||||
});
|
||||
|
||||
// A plain Error is a bug, not a message for the caller — the body must be JSON but must NOT leak
|
||||
// error.message, which nobody wrote for an outside reader.
|
||||
test('a plain Error → 500, { error: "Internal Server Error" }, message not leaked', async () => {
|
||||
const res = await request(appThatThrows(new Error('secret internal detail')));
|
||||
expect(res.status).toBe(500);
|
||||
expect(res.headers.get('content-type')).toContain('application/json');
|
||||
expect(await res.json()).toEqual({ error: 'Internal Server Error' });
|
||||
});
|
||||
});
|
||||
@@ -228,7 +228,19 @@ const handleRevokedSession = (url: string) => {
|
||||
const validateResponse = async (res: Response) => {
|
||||
if (res.status >= 400) {
|
||||
const { onError } = useClient.config;
|
||||
const message = await res.text();
|
||||
// Read the body ONCE, as text, and parse it ourselves — a Response body cannot be read twice.
|
||||
// The platform answers `{ error: string }` since 2026-08-25, but a plugin's own routes and any
|
||||
// non-Hono surface still answer plain text, so the raw text stays as the fallback.
|
||||
const body = await res.text();
|
||||
let message = body;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(body);
|
||||
if (parsed && typeof parsed === 'object' && typeof (parsed as { error?: unknown }).error === 'string') {
|
||||
message = (parsed as { error: string }).error;
|
||||
}
|
||||
} catch {
|
||||
// Not JSON — keep the text as-is.
|
||||
}
|
||||
if (onError) {
|
||||
onError({ status: res.status, message });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user