From e9a796c069bf2dd13e89507098b14f6f34de0b8c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 7 Feb 2026 13:15:15 +0100 Subject: [PATCH] Replace Buffer with cross-runtime base64 and throw on invalid cursors Use btoa/atob with TextEncoder/TextDecoder instead of Buffer for base64 encoding so the test utility works in browser environments too. Throw InputError for malformed cursors in getEntities and queryEntities to match the catalog backend behavior. Signed-off-by: Patrik Oldsberg Co-authored-by: Cursor --- .../testUtils/InMemoryCatalogClient.test.ts | 9 +++---- .../src/testUtils/InMemoryCatalogClient.ts | 25 +++++++++++++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts index f719204401..d867ab1d28 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts @@ -683,12 +683,11 @@ describe('InMemoryCatalogClient', () => { ]); }); - it('returns empty result for invalid cursor', async () => { + it('throws InputError for invalid cursor', async () => { const client = new InMemoryCatalogClient({ entities }); - const result = await client.queryEntities({ - cursor: 'not-valid-base64!', - }); - expect(result).toEqual({ items: [], pageInfo: {}, totalItems: 0 }); + await expect( + client.queryEntities({ cursor: 'not-valid-base64!' }), + ).rejects.toThrow('Invalid cursor'); }); }); diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index 0c93148c07..48111c96cf 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -43,7 +43,11 @@ import { parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; -import { NotFoundError, NotImplementedError } from '@backstage/errors'; +import { + InputError, + NotFoundError, + NotImplementedError, +} from '@backstage/errors'; import lodash from 'lodash'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { traverse } from '../../../../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch'; @@ -53,12 +57,23 @@ import type { } from '@backstage/plugin-catalog-common'; import { DEFAULT_STREAM_ENTITIES_LIMIT } from '../constants.ts'; +function base64Encode(str: string): string { + const bytes = new TextEncoder().encode(str); + return btoa(Array.from(bytes, b => String.fromCodePoint(b)).join('')); +} + +function base64Decode(str: string): string { + const bin = atob(str); + const bytes = Uint8Array.from(bin, c => c.codePointAt(0)!); + return new TextDecoder().decode(bytes); +} + function makeCursor(data: Record): string { - return Buffer.from(JSON.stringify(data)).toString('base64'); + return base64Encode(JSON.stringify(data)); } function parseCursor(cursor: string): Record { - return JSON.parse(Buffer.from(cursor, 'base64').toString('utf8')); + return JSON.parse(base64Decode(cursor)); } // CATALOG_FILTER_EXISTS is a Symbol that doesn't survive JSON serialization, @@ -328,7 +343,7 @@ export class InMemoryCatalogClient implements CatalogApi { if (cursor.offset !== undefined) offset = cursor.offset as number; if (cursor.limit !== undefined) limit = cursor.limit as number; } catch { - // ignore invalid cursor + throw new InputError('Invalid cursor'); } } @@ -376,7 +391,7 @@ export class InMemoryCatalogClient implements CatalogApi { try { c = parseCursor(request.cursor); } catch { - return { items: [], pageInfo: {}, totalItems: 0 }; + throw new InputError('Invalid cursor'); } filter = deserializeFilter(c.filter as any[]); orderFields = c.orderFields as EntityOrderQuery | undefined;