From 6d45431452fa374e3ef5d55a82b1795fc8b5d68b Mon Sep 17 00:00:00 2001 From: Dawid Malinowski Date: Sat, 21 Feb 2026 19:21:18 +0100 Subject: [PATCH] feat: add GET /internal/caches endpoint to list cache entries Supports optional filtering by `key` (prefix match) and `version`, with `page`/`limit` pagination (default 20, max 100). Returns `{ items, total, page, limit }`. Adds e2e tests covering default listing, key/version filtering, pagination, and input validation. --- routes/internal/caches.get.ts | 41 ++++++++++++++++++++ tests/e2e.test.ts | 70 +++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 routes/internal/caches.get.ts diff --git a/routes/internal/caches.get.ts b/routes/internal/caches.get.ts new file mode 100644 index 0000000..1e1f528 --- /dev/null +++ b/routes/internal/caches.get.ts @@ -0,0 +1,41 @@ +import { z } from 'zod' +import { getDatabase } from '~/lib/db' + +const querySchema = z.object({ + key: z.string().optional(), + version: z.string().optional(), + page: z.coerce.number().int().min(0).default(0), + limit: z.coerce.number().int().min(1).max(100).default(20), +}) + +export default defineEventHandler(async (event) => { + const parsed = querySchema.safeParse(getQuery(event)) + if (!parsed.success) + throw createError({ statusCode: 400, statusMessage: parsed.error.message }) + + const { key, version, page, limit } = parsed.data + const db = await getDatabase() + + let query = db + .selectFrom('cache_entries') + .selectAll() + .orderBy('updatedAt', 'desc') + .limit(limit) + .offset(page * limit) + + if (key) query = query.where('key', 'like', `${key}%`) + if (version) query = query.where('version', '=', version) + + const [items, total] = await Promise.all([ + query.execute(), + db + .selectFrom('cache_entries') + .select((eb) => eb.fn.countAll().as('count')) + .$if(!!key, (q) => q.where('key', 'like', `${key}%`)) + .$if(!!version, (q) => q.where('version', '=', version!)) + .executeTakeFirstOrThrow() + .then((r) => Number(r.count)), + ]) + + return { items, total, page, limit } +}) diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index a1a993f..bd09430 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -9,6 +9,76 @@ import { TEST_TEMP_DIR } from './setup' const testFilePath = path.join(TEST_TEMP_DIR, 'test.bin') const MB = 1024 * 1024 +const BASE_URL = process.env.API_BASE_URL! + +describe('GET /internal/caches', () => { + beforeAll(async () => { + process.env.ACTIONS_CACHE_SERVICE_V2 = 'true' + process.env.ACTIONS_RUNTIME_TOKEN = 'mock-runtime-token' + + // seed two distinct cache entries + const buf = crypto.randomBytes(1024) + await fs.writeFile(testFilePath, buf) + await saveCache([testFilePath], 'list-test-key-alpha') + await saveCache([testFilePath], 'list-test-key-beta') + }) + afterAll(() => { + delete process.env.ACTIONS_CACHE_SERVICE_V2 + delete process.env.ACTIONS_RUNTIME_TOKEN + }) + + test('returns all entries with default pagination', async () => { + const res = await fetch(`${BASE_URL}/internal/caches`) + expect(res.status).toBe(200) + const body = await res.json() as { items: unknown[]; total: number; page: number; limit: number } + expect(body.page).toBe(0) + expect(body.limit).toBe(20) + expect(typeof body.total).toBe('number') + expect(Array.isArray(body.items)).toBe(true) + expect(body.total).toBeGreaterThanOrEqual(2) + }) + + test('filters by key prefix', async () => { + const res = await fetch(`${BASE_URL}/internal/caches?key=list-test-key-alpha`) + expect(res.status).toBe(200) + const body = await res.json() as { items: Array<{ key: string }>; total: number } + expect(body.total).toBeGreaterThanOrEqual(1) + for (const item of body.items) { + expect(item.key).toMatch(/^list-test-key-alpha/) + } + }) + + test('filters by version returns only matching version', async () => { + // fetch the version of a known entry from the listing, then filter by it + const all = await fetch(`${BASE_URL}/internal/caches?key=list-test-key-alpha`) + const allBody = await all.json() as { items: Array<{ key: string; version: string }> } + const knownVersion = allBody.items[0]!.version + + const res = await fetch(`${BASE_URL}/internal/caches?version=${knownVersion}`) + expect(res.status).toBe(200) + const body = await res.json() as { items: Array<{ version: string }> } + for (const item of body.items) { + expect(item.version).toBe(knownVersion) + } + }) + + test('pagination with limit=1', async () => { + const resP0 = await fetch(`${BASE_URL}/internal/caches?limit=1&page=0`) + const resP1 = await fetch(`${BASE_URL}/internal/caches?limit=1&page=1`) + expect(resP0.status).toBe(200) + expect(resP1.status).toBe(200) + const bodyP0 = await resP0.json() as { items: Array<{ id: string }> } + const bodyP1 = await resP1.json() as { items: Array<{ id: string }> } + expect(bodyP0.items).toHaveLength(1) + expect(bodyP1.items).toHaveLength(1) + expect(bodyP0.items[0]!.id).not.toBe(bodyP1.items[0]!.id) + }) + + test('returns 400 for invalid query params', async () => { + const res = await fetch(`${BASE_URL}/internal/caches?limit=999`) + expect(res.status).toBe(400) + }) +}) describe(`save and restore cache with @actions/cache package`, () => { beforeAll(() => {