Skip to content

Commit dc1ee22

Browse files
committed
test: add tests, run format
1 parent 24e9bad commit dc1ee22

File tree

1 file changed

+355
-0
lines changed

1 file changed

+355
-0
lines changed
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
import { describe, expect, it, vi, beforeEach } from 'vitest'
2+
import { createError, type H3Event } from 'h3'
3+
import type { Packument, PackumentVersion } from '#shared/types/npm-registry'
4+
5+
const fetchNpmPackageMock = vi.fn()
6+
vi.stubGlobal('fetchNpmPackage', fetchNpmPackageMock)
7+
vi.stubGlobal('defineCachedEventHandler', (fn: Function) => fn)
8+
vi.stubGlobal('CACHE_MAX_AGE_FIVE_MINUTES', 300)
9+
10+
const handleApiErrorMock = vi.fn(
11+
(_error: unknown, fallback: { statusCode: number; message: string }) => {
12+
throw createError(fallback)
13+
},
14+
) as unknown as typeof handleApiError
15+
vi.stubGlobal('handleApiError', handleApiErrorMock)
16+
17+
let routerParam: string | undefined
18+
let queryParams: Record<string, string | number> = {}
19+
20+
vi.stubGlobal('getRouterParam', (_event: unknown, _name: string) => routerParam)
21+
vi.stubGlobal('getQuery', () => queryParams)
22+
vi.stubGlobal('createError', createError)
23+
24+
const handler = (await import('#server/api/registry/timeline/[...pkg].get')).default
25+
26+
function makePackument(opts: {
27+
versions: Record<string, Partial<PackumentVersion>>
28+
time: Record<string, string>
29+
distTags?: Record<string, string>
30+
}): Packument {
31+
return {
32+
'dist-tags': opts.distTags ?? {},
33+
'versions': Object.fromEntries(
34+
Object.entries(opts.versions).map(([v, data]) => [v, { version: v, ...data }]),
35+
),
36+
'time': opts.time,
37+
} as Packument
38+
}
39+
40+
const fakeEvent = {} as H3Event
41+
42+
describe('timeline API', () => {
43+
beforeEach(() => {
44+
vi.clearAllMocks()
45+
routerParam = undefined
46+
queryParams = {}
47+
})
48+
49+
it('throws 404 when package name param is missing', async () => {
50+
routerParam = undefined
51+
await expect(handler(fakeEvent)).rejects.toMatchObject({
52+
statusCode: 404,
53+
})
54+
})
55+
56+
it('throws 400 for invalid URI encoding', async () => {
57+
routerParam = '%E0%A4%A'
58+
await expect(handler(fakeEvent)).rejects.toMatchObject({
59+
statusCode: 400,
60+
})
61+
})
62+
63+
it('returns paginated versions sorted newest-first', async () => {
64+
routerParam = 'my-pkg'
65+
queryParams = { offset: 0, limit: 10 }
66+
67+
fetchNpmPackageMock.mockResolvedValue(
68+
makePackument({
69+
versions: {
70+
'1.0.0': { license: 'MIT' },
71+
'2.0.0': { license: 'ISC' },
72+
'3.0.0': { license: 'MIT' },
73+
},
74+
time: {
75+
'1.0.0': '2024-01-01T00:00:00Z',
76+
'2.0.0': '2024-06-01T00:00:00Z',
77+
'3.0.0': '2025-01-01T00:00:00Z',
78+
},
79+
distTags: { latest: '3.0.0' },
80+
}),
81+
)
82+
83+
const result = await handler(fakeEvent)
84+
expect(result.total).toBe(3)
85+
expect(result.versions).toHaveLength(3)
86+
// newest first
87+
expect(result.versions[0]!.version).toBe('3.0.0')
88+
expect(result.versions[1]!.version).toBe('2.0.0')
89+
expect(result.versions[2]!.version).toBe('1.0.0')
90+
})
91+
92+
it('applies offset and limit correctly', async () => {
93+
routerParam = 'my-pkg'
94+
queryParams = { offset: 1, limit: 1 }
95+
96+
fetchNpmPackageMock.mockResolvedValue(
97+
makePackument({
98+
versions: {
99+
'1.0.0': {},
100+
'2.0.0': {},
101+
'3.0.0': {},
102+
},
103+
time: {
104+
'1.0.0': '2024-01-01T00:00:00Z',
105+
'2.0.0': '2024-06-01T00:00:00Z',
106+
'3.0.0': '2025-01-01T00:00:00Z',
107+
},
108+
}),
109+
)
110+
111+
const result = await handler(fakeEvent)
112+
expect(result.total).toBe(3)
113+
expect(result.versions).toHaveLength(1)
114+
// sorted newest first: 3.0.0, 2.0.0, 1.0.0 → offset 1 = 2.0.0
115+
expect(result.versions[0]!.version).toBe('2.0.0')
116+
})
117+
118+
it('defaults offset to 0 and limit to 25', async () => {
119+
routerParam = 'my-pkg'
120+
queryParams = {}
121+
122+
const versions: Record<string, {}> = {}
123+
const time: Record<string, string> = {}
124+
for (let i = 1; i <= 30; i++) {
125+
const v = `1.0.${i}`
126+
versions[v] = {}
127+
time[v] = new Date(2024, 0, i).toISOString()
128+
}
129+
130+
fetchNpmPackageMock.mockResolvedValue(makePackument({ versions, time }))
131+
132+
const result = await handler(fakeEvent)
133+
expect(result.total).toBe(30)
134+
expect(result.versions).toHaveLength(25)
135+
})
136+
137+
it('clamps limit to max 100', async () => {
138+
routerParam = 'my-pkg'
139+
queryParams = { limit: 999 }
140+
141+
const versions: Record<string, {}> = {}
142+
const time: Record<string, string> = {}
143+
for (let i = 1; i <= 150; i++) {
144+
const v = `1.0.${i}`
145+
versions[v] = {}
146+
time[v] = new Date(2024, 0, (i % 28) + 1, i).toISOString()
147+
}
148+
149+
fetchNpmPackageMock.mockResolvedValue(makePackument({ versions, time }))
150+
151+
const result = await handler(fakeEvent)
152+
expect(result.versions).toHaveLength(100)
153+
})
154+
155+
it('extracts license string from object format', async () => {
156+
routerParam = 'my-pkg'
157+
158+
fetchNpmPackageMock.mockResolvedValue(
159+
makePackument({
160+
versions: {
161+
'1.0.0': { license: { type: 'Apache-2.0' } as never },
162+
},
163+
time: { '1.0.0': '2024-01-01T00:00:00Z' },
164+
}),
165+
)
166+
167+
const result = await handler(fakeEvent)
168+
expect(result.versions[0]!.license).toBe('Apache-2.0')
169+
})
170+
171+
it('includes tags for versions with dist-tags', async () => {
172+
routerParam = 'my-pkg'
173+
174+
fetchNpmPackageMock.mockResolvedValue(
175+
makePackument({
176+
versions: {
177+
'1.0.0': {},
178+
'2.0.0-beta.1': {},
179+
},
180+
time: {
181+
'1.0.0': '2024-01-01T00:00:00Z',
182+
'2.0.0-beta.1': '2024-06-01T00:00:00Z',
183+
},
184+
distTags: { latest: '1.0.0', next: '2.0.0-beta.1' },
185+
}),
186+
)
187+
188+
const result = await handler(fakeEvent)
189+
const latest = result.versions.find((v: any) => v.version === '1.0.0')
190+
const next = result.versions.find((v: any) => v.version === '2.0.0-beta.1')
191+
expect(latest?.tags).toEqual(['latest'])
192+
expect(next?.tags).toEqual(['next'])
193+
})
194+
195+
it('includes module type when present', async () => {
196+
routerParam = 'my-pkg'
197+
198+
fetchNpmPackageMock.mockResolvedValue(
199+
makePackument({
200+
versions: {
201+
'1.0.0': { type: 'module' },
202+
},
203+
time: { '1.0.0': '2024-01-01T00:00:00Z' },
204+
}),
205+
)
206+
207+
const result = await handler(fakeEvent)
208+
expect(result.versions[0]!.type).toBe('module')
209+
})
210+
211+
it('sets hasTypes when version has types field', async () => {
212+
routerParam = 'my-pkg'
213+
214+
fetchNpmPackageMock.mockResolvedValue(
215+
makePackument({
216+
versions: {
217+
'1.0.0': { types: './index.d.ts' },
218+
},
219+
time: { '1.0.0': '2024-01-01T00:00:00Z' },
220+
}),
221+
)
222+
223+
const result = await handler(fakeEvent)
224+
expect(result.versions[0]!.hasTypes).toBe(true)
225+
})
226+
227+
it('sets hasTrustedPublisher when trustedPublisher is true', async () => {
228+
routerParam = 'my-pkg'
229+
230+
fetchNpmPackageMock.mockResolvedValue(
231+
makePackument({
232+
versions: {
233+
'1.0.0': { _npmUser: { trustedPublisher: true, name: 'bob' } },
234+
},
235+
time: { '1.0.0': '2024-01-01T00:00:00Z' },
236+
}),
237+
)
238+
239+
const result = await handler(fakeEvent)
240+
expect(result.versions[0]!.hasTrustedPublisher).toBe(true)
241+
})
242+
243+
it('sets hasProvenance when attestations exist', async () => {
244+
routerParam = 'my-pkg'
245+
246+
fetchNpmPackageMock.mockResolvedValue(
247+
makePackument({
248+
versions: {
249+
'1.0.0': {
250+
dist: {
251+
shasum: 'abc123',
252+
tarball: 'https://registry.npmjs.org/my-pkg/-/my-pkg-1.0.0.tgz',
253+
signatures: [],
254+
attestations: {
255+
url: 'https://example.com',
256+
provenance: {
257+
predicateType: 'https://npmx.dev/provenance/v9.99',
258+
},
259+
},
260+
},
261+
},
262+
},
263+
time: { '1.0.0': '2024-01-01T00:00:00Z' },
264+
}),
265+
)
266+
267+
const result = await handler(fakeEvent)
268+
expect(result.versions[0]!.hasProvenance).toBe(true)
269+
})
270+
271+
it('omits optional fields when not present', async () => {
272+
routerParam = 'my-pkg'
273+
274+
fetchNpmPackageMock.mockResolvedValue(
275+
makePackument({
276+
versions: {
277+
'1.0.0': {},
278+
},
279+
time: { '1.0.0': '2024-01-01T00:00:00Z' },
280+
}),
281+
)
282+
283+
const result = await handler(fakeEvent)
284+
const v = result.versions[0]!
285+
expect(v.license).toBeUndefined()
286+
expect(v.type).toBeUndefined()
287+
expect(v.hasTypes).toBeUndefined()
288+
expect(v.hasTrustedPublisher).toBeUndefined()
289+
expect(v.hasProvenance).toBeUndefined()
290+
expect(v.tags).toEqual([])
291+
})
292+
293+
it('skips versions without a time entry', async () => {
294+
routerParam = 'my-pkg'
295+
296+
fetchNpmPackageMock.mockResolvedValue(
297+
makePackument({
298+
versions: {
299+
'1.0.0': {},
300+
'2.0.0': {},
301+
},
302+
time: {
303+
'1.0.0': '2024-01-01T00:00:00Z',
304+
// no time for 2.0.0
305+
},
306+
}),
307+
)
308+
309+
const result = await handler(fakeEvent)
310+
expect(result.total).toBe(1)
311+
expect(result.versions[0]!.version).toBe('1.0.0')
312+
})
313+
314+
it('decodes scoped package names', async () => {
315+
routerParam = '%40scope%2Fmy-pkg'
316+
317+
fetchNpmPackageMock.mockResolvedValue(
318+
makePackument({
319+
versions: { '1.0.0': {} },
320+
time: { '1.0.0': '2024-01-01T00:00:00Z' },
321+
}),
322+
)
323+
324+
await handler(fakeEvent)
325+
expect(fetchNpmPackageMock).toHaveBeenCalledWith('@scope/my-pkg')
326+
})
327+
328+
it('calls handleApiError when fetchNpmPackage throws', async () => {
329+
routerParam = 'my-pkg'
330+
const error = new Error('upstream failure')
331+
fetchNpmPackageMock.mockRejectedValue(error)
332+
333+
await expect(handler(fakeEvent)).rejects.toThrow('Failed to fetch timeline for my-pkg')
334+
expect(handleApiErrorMock).toHaveBeenCalledWith(error, {
335+
statusCode: 502,
336+
message: 'Failed to fetch timeline for my-pkg',
337+
})
338+
})
339+
340+
it('supports multiple tags on the same version', async () => {
341+
routerParam = 'my-pkg'
342+
343+
fetchNpmPackageMock.mockResolvedValue(
344+
makePackument({
345+
versions: { '1.0.0': {} },
346+
time: { '1.0.0': '2024-01-01T00:00:00Z' },
347+
distTags: { latest: '1.0.0', stable: '1.0.0' },
348+
}),
349+
)
350+
351+
const result = await handler(fakeEvent)
352+
expect(result.versions[0]!.tags).toEqual(expect.arrayContaining(['latest', 'stable']))
353+
expect(result.versions[0]!.tags).toHaveLength(2)
354+
})
355+
})

0 commit comments

Comments
 (0)