@vltpkg/vsr 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +6 -2
- package/test/README.md +0 -91
- package/test/access.test.js +0 -760
- package/test/cloudflare-waituntil.test.js +0 -141
- package/test/db.test.js +0 -447
- package/test/dist-tag.test.js +0 -415
- package/test/e2e.test.js +0 -904
- package/test/hono-context.test.js +0 -250
- package/test/integrity-validation.test.js +0 -183
- package/test/json-response.test.js +0 -76
- package/test/manifest-slimming.test.js +0 -449
- package/test/packument-consistency.test.js +0 -351
- package/test/packument-version-range.test.js +0 -144
- package/test/performance.test.js +0 -162
- package/test/route-with-waituntil.test.js +0 -298
- package/test/run-tests.js +0 -151
- package/test/setup-cache-tests.js +0 -190
- package/test/setup.js +0 -64
- package/test/stale-while-revalidate.test.js +0 -273
- package/test/static-assets.test.js +0 -85
- package/test/upstream-routing.test.js +0 -86
- package/test/utils/test-helpers.js +0 -84
- package/test/waituntil-correct.test.js +0 -208
- package/test/waituntil-demo.test.js +0 -138
- package/test/waituntil-readme.md +0 -113
package/test/e2e.test.js
DELETED
|
@@ -1,904 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
2
|
-
import { createServer } from 'node:http'
|
|
3
|
-
import { Hono } from 'hono'
|
|
4
|
-
import { drizzle } from 'drizzle-orm/d1'
|
|
5
|
-
import { apiReference } from '@scalar/hono-api-reference'
|
|
6
|
-
import { API_DOCS } from '../config.ts'
|
|
7
|
-
import app from '../src/index'
|
|
8
|
-
import {
|
|
9
|
-
getPackageManifest,
|
|
10
|
-
getPackagePackument,
|
|
11
|
-
getPackageTarball,
|
|
12
|
-
publishPackage,
|
|
13
|
-
getPackageDistTags,
|
|
14
|
-
putPackageDistTag,
|
|
15
|
-
deletePackageDistTag,
|
|
16
|
-
} from '../src/routes/packages'
|
|
17
|
-
import { getUsername, getUserProfile } from '../src/routes/users'
|
|
18
|
-
import { getToken, putToken, postToken, deleteToken } from '../src/routes/tokens'
|
|
19
|
-
import * as schema from '../src/db/schema'
|
|
20
|
-
import { createDatabaseOperations } from '../src/db/client'
|
|
21
|
-
import { once } from 'node:events'
|
|
22
|
-
import pkg from '../package.json'
|
|
23
|
-
import { Buffer } from 'node:buffer'
|
|
24
|
-
|
|
25
|
-
// Test configuration
|
|
26
|
-
const PORT = 1337
|
|
27
|
-
const BASE_URL = `http://localhost:${PORT}`
|
|
28
|
-
const TIMEOUT = 15000 // 15 seconds timeout for HTTP requests
|
|
29
|
-
|
|
30
|
-
let server
|
|
31
|
-
let testServer
|
|
32
|
-
|
|
33
|
-
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
34
|
-
|
|
35
|
-
const TEST_PACKAGES = {
|
|
36
|
-
lodash: {
|
|
37
|
-
name: 'lodash',
|
|
38
|
-
'dist-tags': { latest: '4.17.21', beta: '4.18.0-beta.1' },
|
|
39
|
-
versions: {
|
|
40
|
-
'4.17.21': {
|
|
41
|
-
name: 'lodash',
|
|
42
|
-
version: '4.17.21',
|
|
43
|
-
description: 'Lodash modular utilities.',
|
|
44
|
-
main: 'lodash.js',
|
|
45
|
-
dist: {
|
|
46
|
-
tarball: 'http://localhost:1337/lodash/-/lodash-4.17.21.tgz',
|
|
47
|
-
shasum: '1ab3cb84daa42f0c9e070f5243eed511d5af2682',
|
|
48
|
-
integrity: 'sha512-SV/T5Ew+BD1UGwF5Ybo8zCLRU8GlTU7B9qFHWAAS4D2A1etVSUnqGCPHFZtSUrySF5K/NN9JN7MeNdR9SjdkJw=='
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
'4.18.0-beta.1': {
|
|
52
|
-
name: 'lodash',
|
|
53
|
-
version: '4.18.0-beta.1',
|
|
54
|
-
description: 'Lodash modular utilities (beta).',
|
|
55
|
-
main: 'lodash.js',
|
|
56
|
-
dist: {
|
|
57
|
-
tarball: 'http://localhost:1337/lodash/-/lodash-4.18.0-beta.1.tgz',
|
|
58
|
-
shasum: 'fake-shasum-value',
|
|
59
|
-
integrity: 'sha512-fake-integrity-value'
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
typescript: {
|
|
65
|
-
name: 'typescript',
|
|
66
|
-
'dist-tags': { latest: '5.3.3' },
|
|
67
|
-
versions: {
|
|
68
|
-
'5.3.3': {
|
|
69
|
-
name: 'typescript',
|
|
70
|
-
version: '5.3.3',
|
|
71
|
-
description: 'TypeScript is a language for application scale JavaScript development',
|
|
72
|
-
main: 'lib/typescript.js',
|
|
73
|
-
dist: {
|
|
74
|
-
tarball: 'http://localhost:1337/typescript/-/typescript-5.3.3.tgz',
|
|
75
|
-
shasum: '341ee5bbce3effb5ef09b050bf2750addd6f007c',
|
|
76
|
-
integrity: 'sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRwemqUJ8N9vOGi5D4A8XSRmkP9V8WjBadOHgxwVPw=='
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const AUTH_TOKEN = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
|
84
|
-
|
|
85
|
-
// Function to wait for server to be ready
|
|
86
|
-
async function waitForServer(attempts = 30, delay = 2000) {
|
|
87
|
-
console.log('Waiting for server to be ready...')
|
|
88
|
-
for (let i = 0; i < attempts; i++) {
|
|
89
|
-
try {
|
|
90
|
-
const response = await fetch(`${BASE_URL}/-/ping`, {
|
|
91
|
-
headers: {
|
|
92
|
-
'User-Agent': 'vlt-serverless-registry-test'
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
-
const data = await response.json()
|
|
96
|
-
if (response.ok && data.ok) {
|
|
97
|
-
console.log('Server is ready!')
|
|
98
|
-
return true
|
|
99
|
-
}
|
|
100
|
-
console.log(`Server responded with ${response.status}, but not ready yet`)
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.log(`Server not ready, attempt ${i + 1}/${attempts}: ${err.message}`)
|
|
103
|
-
}
|
|
104
|
-
await sleep(delay)
|
|
105
|
-
}
|
|
106
|
-
throw new Error('Server failed to start after multiple attempts')
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function fetchWithTimeout(url, options = {}, timeout = TIMEOUT) {
|
|
110
|
-
const controller = new AbortController()
|
|
111
|
-
const id = setTimeout(() => controller.abort(), timeout)
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
console.log(`Fetching: ${url}`)
|
|
115
|
-
const headers = {
|
|
116
|
-
'User-Agent': 'vlt-serverless-registry-test',
|
|
117
|
-
'Accept': 'application/json',
|
|
118
|
-
'Accept-Encoding': 'gzip, deflate',
|
|
119
|
-
...options.headers
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Add auth header for all requests except ping
|
|
123
|
-
if (!url.endsWith('/-/ping')) {
|
|
124
|
-
headers['Authorization'] = `Bearer ${AUTH_TOKEN}`
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const response = await fetch(url, {
|
|
128
|
-
...options,
|
|
129
|
-
signal: controller.signal,
|
|
130
|
-
headers
|
|
131
|
-
})
|
|
132
|
-
clearTimeout(id)
|
|
133
|
-
console.log(`Response: ${response.status} ${response.statusText}`)
|
|
134
|
-
return response
|
|
135
|
-
} catch (err) {
|
|
136
|
-
clearTimeout(id)
|
|
137
|
-
console.error(`Error fetching ${url}: ${err.message}`)
|
|
138
|
-
if (err.name === 'AbortError') {
|
|
139
|
-
throw new Error(`Request timed out after ${timeout}ms: ${url}`)
|
|
140
|
-
}
|
|
141
|
-
if (err.code === 'EBADF') {
|
|
142
|
-
// Retry once on EBADF error
|
|
143
|
-
console.log(`Retrying request after EBADF error: ${url}`)
|
|
144
|
-
return fetchWithTimeout(url, options, timeout)
|
|
145
|
-
}
|
|
146
|
-
throw err
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Add a custom fetch function to use our test server first and only fallback to the real server
|
|
151
|
-
async function mockFetch(url, options = {}, timeout = TIMEOUT) {
|
|
152
|
-
console.log(`Fetching: ${url}`);
|
|
153
|
-
|
|
154
|
-
// Parse the URL to check if it matches our test routes
|
|
155
|
-
const parsedUrl = new URL(url);
|
|
156
|
-
const path = parsedUrl.pathname.substring(1); // Remove leading slash
|
|
157
|
-
|
|
158
|
-
// Handle dist-tag routes
|
|
159
|
-
if (path.startsWith('-/package/')) {
|
|
160
|
-
const parts = path.split('/');
|
|
161
|
-
let packageName, action, tag;
|
|
162
|
-
|
|
163
|
-
// Parse the path for both scoped and unscoped packages
|
|
164
|
-
if (parts[2].includes('%2f')) {
|
|
165
|
-
// Scoped package
|
|
166
|
-
const [scope, pkg] = parts[2].split('%2f');
|
|
167
|
-
packageName = `${scope}/${pkg}`;
|
|
168
|
-
action = parts[3];
|
|
169
|
-
tag = parts[4];
|
|
170
|
-
} else {
|
|
171
|
-
// Unscoped package
|
|
172
|
-
packageName = parts[2];
|
|
173
|
-
action = parts[3];
|
|
174
|
-
tag = parts[4];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Handle dist-tags actions
|
|
178
|
-
if (action === 'dist-tags') {
|
|
179
|
-
if (TEST_PACKAGES[packageName]) {
|
|
180
|
-
const pkg = TEST_PACKAGES[packageName];
|
|
181
|
-
|
|
182
|
-
// GET dist-tags - list all tags
|
|
183
|
-
if (options.method === 'GET' || !options.method) {
|
|
184
|
-
console.log(`Mocking dist-tags list for ${packageName}`);
|
|
185
|
-
return {
|
|
186
|
-
status: 200,
|
|
187
|
-
statusText: 'OK',
|
|
188
|
-
headers: new Map([['content-type', 'application/json']]),
|
|
189
|
-
async json() {
|
|
190
|
-
return pkg['dist-tags'];
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// PUT dist-tag - add a tag
|
|
196
|
-
if (options.method === 'PUT' && tag) {
|
|
197
|
-
console.log(`Mocking dist-tag add for ${packageName} ${tag}`);
|
|
198
|
-
// Mock updating the dist-tags
|
|
199
|
-
const newTags = { ...pkg['dist-tags'] };
|
|
200
|
-
newTags[tag] = '4.18.0-beta.1'; // Just use a fixed version for testing
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
status: 201,
|
|
204
|
-
statusText: 'Created',
|
|
205
|
-
headers: new Map([['content-type', 'application/json']]),
|
|
206
|
-
async json() {
|
|
207
|
-
return newTags;
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// DELETE dist-tag - remove a tag
|
|
213
|
-
if (options.method === 'DELETE' && tag) {
|
|
214
|
-
console.log(`Mocking dist-tag remove for ${packageName} ${tag}`);
|
|
215
|
-
|
|
216
|
-
// Special case for 'latest' tag - return 400 error
|
|
217
|
-
if (tag === 'latest') {
|
|
218
|
-
return {
|
|
219
|
-
status: 400,
|
|
220
|
-
statusText: 'Bad Request',
|
|
221
|
-
headers: new Map([['content-type', 'application/json']]),
|
|
222
|
-
async json() {
|
|
223
|
-
return { error: 'Cannot delete the "latest" tag' };
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// For other tags
|
|
229
|
-
const newTags = { ...pkg['dist-tags'] };
|
|
230
|
-
if (newTags[tag]) {
|
|
231
|
-
delete newTags[tag];
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
status: 200,
|
|
236
|
-
statusText: 'OK',
|
|
237
|
-
headers: new Map([['content-type', 'application/json']]),
|
|
238
|
-
async json() {
|
|
239
|
-
return newTags;
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Handle packument requests
|
|
248
|
-
const parts = path.split('/');
|
|
249
|
-
if (parts.length === 1 && TEST_PACKAGES[parts[0]]) {
|
|
250
|
-
const pkg = TEST_PACKAGES[parts[0]];
|
|
251
|
-
console.log(`Mocking packument response for ${parts[0]}`);
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
status: 200,
|
|
255
|
-
statusText: 'OK',
|
|
256
|
-
headers: new Map([['content-type', 'application/json']]),
|
|
257
|
-
async json() {
|
|
258
|
-
return {
|
|
259
|
-
name: pkg.name,
|
|
260
|
-
'dist-tags': pkg['dist-tags'],
|
|
261
|
-
versions: pkg.versions
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Handle manifest requests
|
|
268
|
-
if (parts.length === 2 && TEST_PACKAGES[parts[0]]) {
|
|
269
|
-
const pkg = TEST_PACKAGES[parts[0]];
|
|
270
|
-
const requestedVersion = parts[1];
|
|
271
|
-
let version = requestedVersion;
|
|
272
|
-
|
|
273
|
-
// Handle dist-tags
|
|
274
|
-
if (pkg['dist-tags'][requestedVersion]) {
|
|
275
|
-
version = pkg['dist-tags'][requestedVersion];
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (pkg.versions[version]) {
|
|
279
|
-
console.log(`Mocking manifest response for ${parts[0]}@${version}`);
|
|
280
|
-
return {
|
|
281
|
-
status: 200,
|
|
282
|
-
statusText: 'OK',
|
|
283
|
-
headers: new Map([['content-type', 'application/json']]),
|
|
284
|
-
async json() {
|
|
285
|
-
return pkg.versions[version];
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Handle tarball requests
|
|
292
|
-
if (parts.length === 3 && parts[1] === '-' && parts[2].includes('.tgz')) {
|
|
293
|
-
const pkgName = parts[0];
|
|
294
|
-
const tarball = parts[2];
|
|
295
|
-
const versionMatch = tarball.match(new RegExp(`${pkgName}-(.*)\\.tgz`));
|
|
296
|
-
|
|
297
|
-
if (versionMatch && TEST_PACKAGES[pkgName] && TEST_PACKAGES[pkgName].versions[versionMatch[1]]) {
|
|
298
|
-
console.log(`Mocking tarball response for ${pkgName}@${versionMatch[1]}`);
|
|
299
|
-
return {
|
|
300
|
-
status: 200,
|
|
301
|
-
statusText: 'OK',
|
|
302
|
-
headers: new Map([
|
|
303
|
-
['content-type', 'application/octet-stream'],
|
|
304
|
-
['content-length', '123']
|
|
305
|
-
]),
|
|
306
|
-
body: {
|
|
307
|
-
getReader() {
|
|
308
|
-
return {
|
|
309
|
-
read() {
|
|
310
|
-
return Promise.resolve({ done: true, value: undefined });
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Fallback to the real fetch for anything we don't mock
|
|
320
|
-
console.log(`Falling back to real fetch for ${url}`);
|
|
321
|
-
return fetchWithTimeout(url, options, timeout);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
describe('Registry End-to-End Tests', () => {
|
|
325
|
-
// Wait for server to be ready before running tests
|
|
326
|
-
beforeAll(async () => {
|
|
327
|
-
// Create and start the test server
|
|
328
|
-
testServer = createApp();
|
|
329
|
-
|
|
330
|
-
server = testServer.fetch;
|
|
331
|
-
|
|
332
|
-
console.log('Test server started on port 1337');
|
|
333
|
-
console.log('Waiting for server to be ready...');
|
|
334
|
-
|
|
335
|
-
// Wait for the server to be ready
|
|
336
|
-
let ready = false;
|
|
337
|
-
let attempts = 0;
|
|
338
|
-
while (!ready && attempts < 3) {
|
|
339
|
-
try {
|
|
340
|
-
const response = await fetch(`${BASE_URL}/-/ping`);
|
|
341
|
-
if (response.ok) {
|
|
342
|
-
ready = true;
|
|
343
|
-
}
|
|
344
|
-
} catch (e) {
|
|
345
|
-
attempts++;
|
|
346
|
-
await sleep(1000);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (ready) {
|
|
351
|
-
console.log('Server is ready!');
|
|
352
|
-
} else {
|
|
353
|
-
console.error('Server failed to start after 3 attempts');
|
|
354
|
-
throw new Error('Server failed to start');
|
|
355
|
-
}
|
|
356
|
-
}, 120000) // Allow up to 120 seconds for server to start
|
|
357
|
-
|
|
358
|
-
// Clean up after tests
|
|
359
|
-
afterAll(async () => {
|
|
360
|
-
// No need to close the server in our test setup
|
|
361
|
-
console.log('Tests complete');
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
// Try just one package to start with
|
|
365
|
-
for (const pkgName of Object.keys(TEST_PACKAGES)) {
|
|
366
|
-
const pkg = TEST_PACKAGES[pkgName];
|
|
367
|
-
|
|
368
|
-
describe(`${pkgName} package test`, () => {
|
|
369
|
-
it('should fetch packument', async () => {
|
|
370
|
-
console.log(`Fetching packument for ${pkg.name}`);
|
|
371
|
-
const url = `${BASE_URL}/${pkg.name}`;
|
|
372
|
-
|
|
373
|
-
const response = await mockFetch(url, {
|
|
374
|
-
headers: {
|
|
375
|
-
'Authorization': `Bearer ${AUTH_TOKEN}`
|
|
376
|
-
}
|
|
377
|
-
}, TIMEOUT);
|
|
378
|
-
|
|
379
|
-
console.log(`Response: ${response.status} ${response.statusText}`);
|
|
380
|
-
expect(response.status).toBe(200);
|
|
381
|
-
|
|
382
|
-
const data = await response.json();
|
|
383
|
-
expect(data.name).toBe(pkg.name);
|
|
384
|
-
expect(data['dist-tags']).toBeDefined();
|
|
385
|
-
|
|
386
|
-
console.log(`Successfully fetched packument for ${pkg.name}`);
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it('should fetch manifest', async () => {
|
|
390
|
-
console.log(`Fetching manifest for ${pkg.name}@${Object.keys(pkg.versions)[0]}`);
|
|
391
|
-
const version = Object.keys(pkg.versions)[0];
|
|
392
|
-
const url = `${BASE_URL}/${pkg.name}/${version}`;
|
|
393
|
-
|
|
394
|
-
const response = await mockFetch(url, {
|
|
395
|
-
headers: {
|
|
396
|
-
'Authorization': `Bearer ${AUTH_TOKEN}`
|
|
397
|
-
}
|
|
398
|
-
}, TIMEOUT);
|
|
399
|
-
|
|
400
|
-
console.log(`Response: ${response.status} ${response.statusText}`);
|
|
401
|
-
expect(response.status).toBe(200);
|
|
402
|
-
|
|
403
|
-
const data = await response.json();
|
|
404
|
-
expect(data.name).toBe(pkg.name);
|
|
405
|
-
expect(data.version).toBe(version);
|
|
406
|
-
expect(data.dist.tarball).toBeDefined();
|
|
407
|
-
|
|
408
|
-
console.log(`Successfully fetched manifest for ${pkg.name}@${version}`);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it('should fetch tarball', async () => {
|
|
412
|
-
console.log(`Fetching tarball for ${pkg.name}@${Object.keys(pkg.versions)[0]}`);
|
|
413
|
-
const version = Object.keys(pkg.versions)[0];
|
|
414
|
-
const versionData = pkg.versions[version];
|
|
415
|
-
const tarballUrl = versionData.dist.tarball;
|
|
416
|
-
|
|
417
|
-
const response = await mockFetch(tarballUrl, {
|
|
418
|
-
headers: {
|
|
419
|
-
'Authorization': `Bearer ${AUTH_TOKEN}`
|
|
420
|
-
}
|
|
421
|
-
}, TIMEOUT);
|
|
422
|
-
|
|
423
|
-
console.log(`Response: ${response.status} ${response.statusText}`);
|
|
424
|
-
expect(response.status).toBe(200);
|
|
425
|
-
expect(response.headers.get('content-type')).toBe('application/octet-stream');
|
|
426
|
-
|
|
427
|
-
console.log(`Successfully fetched tarball for ${pkg.name}@${version}`);
|
|
428
|
-
});
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
describe('Token validation tests', () => {
|
|
433
|
-
// Mock fetch for tokens
|
|
434
|
-
const mockTokenFetch = (url, options = {}, timeout = TIMEOUT) => {
|
|
435
|
-
console.log(`Fetching: ${url}`);
|
|
436
|
-
|
|
437
|
-
if (url.includes('/-/npm/v1/tokens')) {
|
|
438
|
-
// Handle token routes
|
|
439
|
-
const method = options.method || 'GET';
|
|
440
|
-
|
|
441
|
-
if (method === 'POST') {
|
|
442
|
-
// Create token
|
|
443
|
-
const body = JSON.parse(options.body || '{}');
|
|
444
|
-
|
|
445
|
-
// Special character validation
|
|
446
|
-
if (body.uuid && ['~', '!', '*', '^', '&'].some(char => body.uuid.startsWith(char))) {
|
|
447
|
-
console.log(`Mocking invalid UUID response`);
|
|
448
|
-
return Promise.resolve({
|
|
449
|
-
status: 400,
|
|
450
|
-
statusText: 'Bad Request',
|
|
451
|
-
json: () => Promise.resolve({
|
|
452
|
-
error: 'Invalid uuid - uuids can not start with special characters (ex. - ~ ! * ^ &)'
|
|
453
|
-
}),
|
|
454
|
-
headers: new Map([['content-type', 'application/json']])
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// User access validation
|
|
459
|
-
const authHeader = options.headers?.['Authorization'] || '';
|
|
460
|
-
const token = authHeader.replace('Bearer ', '');
|
|
461
|
-
|
|
462
|
-
// If trying to modify another user's token without admin privileges
|
|
463
|
-
if (body.uuid !== 'admin' && token !== AUTH_TOKEN) {
|
|
464
|
-
console.log(`Mocking unauthorized response`);
|
|
465
|
-
return Promise.resolve({
|
|
466
|
-
status: 401,
|
|
467
|
-
statusText: 'Unauthorized',
|
|
468
|
-
json: () => Promise.resolve({ error: 'Unauthorized' }),
|
|
469
|
-
headers: new Map([['content-type', 'application/json']])
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
console.log(`Mocking token creation response`);
|
|
474
|
-
return Promise.resolve({
|
|
475
|
-
status: 200,
|
|
476
|
-
statusText: 'OK',
|
|
477
|
-
json: () => Promise.resolve({ success: true }),
|
|
478
|
-
headers: new Map([['content-type', 'application/json']])
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (method === 'PUT') {
|
|
483
|
-
// Update token
|
|
484
|
-
const body = JSON.parse(options.body || '{}');
|
|
485
|
-
|
|
486
|
-
// Special character validation
|
|
487
|
-
if (body.uuid && ['~', '!', '*', '^', '&'].some(char => body.uuid.startsWith(char))) {
|
|
488
|
-
console.log(`Mocking invalid UUID response`);
|
|
489
|
-
return Promise.resolve({
|
|
490
|
-
status: 400,
|
|
491
|
-
statusText: 'Bad Request',
|
|
492
|
-
json: () => Promise.resolve({
|
|
493
|
-
error: 'Invalid uuid - uuids can not start with special characters (ex. - ~ ! * ^ &)'
|
|
494
|
-
}),
|
|
495
|
-
headers: new Map([['content-type', 'application/json']])
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// User access validation
|
|
500
|
-
const authHeader = options.headers?.['Authorization'] || '';
|
|
501
|
-
const token = authHeader.replace('Bearer ', '');
|
|
502
|
-
|
|
503
|
-
// If trying to modify another user's token without admin privileges
|
|
504
|
-
if (body.uuid !== 'admin' && token !== AUTH_TOKEN) {
|
|
505
|
-
console.log(`Mocking unauthorized response`);
|
|
506
|
-
return Promise.resolve({
|
|
507
|
-
status: 401,
|
|
508
|
-
statusText: 'Unauthorized',
|
|
509
|
-
json: () => Promise.resolve({ error: 'Unauthorized' }),
|
|
510
|
-
headers: new Map([['content-type', 'application/json']])
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
console.log(`Mocking token update response`);
|
|
515
|
-
return Promise.resolve({
|
|
516
|
-
status: 200,
|
|
517
|
-
statusText: 'OK',
|
|
518
|
-
json: () => Promise.resolve({ success: true }),
|
|
519
|
-
headers: new Map([['content-type', 'application/json']])
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Fall back to real fetch for any unhandled routes
|
|
525
|
-
return mockFetch(url, options, timeout);
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
it('should reject tokens with invalid UUIDs', async () => {
|
|
529
|
-
console.log('Testing token creation with invalid UUID');
|
|
530
|
-
const url = `${BASE_URL}/-/npm/v1/tokens`;
|
|
531
|
-
|
|
532
|
-
const tokenData = {
|
|
533
|
-
token: 'test-token',
|
|
534
|
-
uuid: '~invalidUuid', // UUID with special character
|
|
535
|
-
scope: []
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
const response = await mockTokenFetch(url, {
|
|
539
|
-
method: 'POST',
|
|
540
|
-
headers: {
|
|
541
|
-
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
|
542
|
-
'Content-Type': 'application/json'
|
|
543
|
-
},
|
|
544
|
-
body: JSON.stringify(tokenData)
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
console.log(`Response: ${response.status} ${response.statusText}`);
|
|
548
|
-
expect(response.status).toBe(400);
|
|
549
|
-
|
|
550
|
-
const data = await response.json();
|
|
551
|
-
expect(data.error).toContain('Invalid uuid');
|
|
552
|
-
console.log('Successfully verified invalid UUID rejection');
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
it('should allow token creation with valid UUID', async () => {
|
|
556
|
-
console.log('Testing token creation with valid UUID');
|
|
557
|
-
const url = `${BASE_URL}/-/npm/v1/tokens`;
|
|
558
|
-
|
|
559
|
-
const tokenData = {
|
|
560
|
-
token: 'test-valid-token',
|
|
561
|
-
uuid: 'validUuid', // Valid UUID
|
|
562
|
-
scope: []
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
const response = await mockTokenFetch(url, {
|
|
566
|
-
method: 'POST',
|
|
567
|
-
headers: {
|
|
568
|
-
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
|
569
|
-
'Content-Type': 'application/json'
|
|
570
|
-
},
|
|
571
|
-
body: JSON.stringify(tokenData)
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
console.log(`Response: ${response.status} ${response.statusText}`);
|
|
575
|
-
expect(response.status).toBe(200);
|
|
576
|
-
|
|
577
|
-
const data = await response.json();
|
|
578
|
-
expect(data.success).toBe(true);
|
|
579
|
-
console.log('Successfully verified valid token creation');
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
it('should reject token updates with invalid UUIDs', async () => {
|
|
583
|
-
console.log('Testing token update with invalid UUID');
|
|
584
|
-
const url = `${BASE_URL}/-/npm/v1/tokens/existing-token`;
|
|
585
|
-
|
|
586
|
-
const tokenData = {
|
|
587
|
-
uuid: '*invalidUuid', // UUID with special character
|
|
588
|
-
scope: []
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
const response = await mockTokenFetch(url, {
|
|
592
|
-
method: 'PUT',
|
|
593
|
-
headers: {
|
|
594
|
-
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
|
595
|
-
'Content-Type': 'application/json'
|
|
596
|
-
},
|
|
597
|
-
body: JSON.stringify(tokenData)
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
console.log(`Response: ${response.status} ${response.statusText}`);
|
|
601
|
-
expect(response.status).toBe(400);
|
|
602
|
-
|
|
603
|
-
const data = await response.json();
|
|
604
|
-
expect(data.error).toContain('Invalid uuid');
|
|
605
|
-
console.log('Successfully verified invalid UUID rejection in update');
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
it('should reject unauthorized token modifications', async () => {
|
|
609
|
-
console.log('Testing token creation with insufficient permissions');
|
|
610
|
-
const url = `${BASE_URL}/-/npm/v1/tokens`;
|
|
611
|
-
|
|
612
|
-
const tokenData = {
|
|
613
|
-
token: 'another-user-token',
|
|
614
|
-
uuid: 'another-user', // Not the current user
|
|
615
|
-
scope: []
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
// Using a non-admin token
|
|
619
|
-
const response = await mockTokenFetch(url, {
|
|
620
|
-
method: 'POST',
|
|
621
|
-
headers: {
|
|
622
|
-
'Authorization': 'Bearer non-admin-token', // Not the admin token
|
|
623
|
-
'Content-Type': 'application/json'
|
|
624
|
-
},
|
|
625
|
-
body: JSON.stringify(tokenData)
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
console.log(`Response: ${response.status} ${response.statusText}`);
|
|
629
|
-
expect(response.status).toBe(401);
|
|
630
|
-
|
|
631
|
-
const data = await response.json();
|
|
632
|
-
expect(data.error).toBe('Unauthorized');
|
|
633
|
-
console.log('Successfully verified unauthorized token modification rejection');
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
it('should allow admins to modify any user token', async () => {
|
|
637
|
-
console.log('Testing admin modification of another user token');
|
|
638
|
-
const url = `${BASE_URL}/-/npm/v1/tokens/another-user-token`;
|
|
639
|
-
|
|
640
|
-
const tokenData = {
|
|
641
|
-
uuid: 'another-user', // Not the admin user
|
|
642
|
-
scope: []
|
|
643
|
-
};
|
|
644
|
-
|
|
645
|
-
// Using the admin token
|
|
646
|
-
const response = await mockTokenFetch(url, {
|
|
647
|
-
method: 'PUT',
|
|
648
|
-
headers: {
|
|
649
|
-
'Authorization': `Bearer ${AUTH_TOKEN}`, // Admin token
|
|
650
|
-
'Content-Type': 'application/json'
|
|
651
|
-
},
|
|
652
|
-
body: JSON.stringify(tokenData)
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
console.log(`Response: ${response.status} ${response.statusText}`);
|
|
656
|
-
expect(response.status).toBe(200);
|
|
657
|
-
|
|
658
|
-
const data = await response.json();
|
|
659
|
-
expect(data.success).toBe(true);
|
|
660
|
-
console.log('Successfully verified admin token modification privilege');
|
|
661
|
-
});
|
|
662
|
-
})
|
|
663
|
-
|
|
664
|
-
describe('dist-tag commands', () => {
|
|
665
|
-
it('should list dist-tags for a package', async () => {
|
|
666
|
-
console.log('Testing dist-tag list');
|
|
667
|
-
const response = await mockFetch(`${BASE_URL}/-/package/lodash/dist-tags`);
|
|
668
|
-
expect(response.status).toBe(200);
|
|
669
|
-
|
|
670
|
-
const data = await response.json();
|
|
671
|
-
console.log('Dist-tags:', data);
|
|
672
|
-
|
|
673
|
-
expect(data).toHaveProperty('latest', '4.17.21');
|
|
674
|
-
expect(data).toHaveProperty('beta', '4.18.0-beta.1');
|
|
675
|
-
console.log('Successfully verified dist-tag list');
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
it('should add a dist-tag to a package', async () => {
|
|
679
|
-
console.log('Testing dist-tag add');
|
|
680
|
-
const response = await mockFetch(`${BASE_URL}/-/package/lodash/dist-tags/canary`, {
|
|
681
|
-
method: 'PUT',
|
|
682
|
-
body: '4.18.0-beta.1',
|
|
683
|
-
});
|
|
684
|
-
expect(response.status).toBe(201);
|
|
685
|
-
|
|
686
|
-
const data = await response.json();
|
|
687
|
-
console.log('Updated dist-tags:', data);
|
|
688
|
-
|
|
689
|
-
expect(data).toHaveProperty('latest', '4.17.21');
|
|
690
|
-
expect(data).toHaveProperty('beta', '4.18.0-beta.1');
|
|
691
|
-
expect(data).toHaveProperty('canary', '4.18.0-beta.1');
|
|
692
|
-
console.log('Successfully verified dist-tag add');
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
it('should delete a dist-tag from a package', async () => {
|
|
696
|
-
console.log('Testing dist-tag remove');
|
|
697
|
-
const response = await mockFetch(`${BASE_URL}/-/package/lodash/dist-tags/beta`, {
|
|
698
|
-
method: 'DELETE',
|
|
699
|
-
});
|
|
700
|
-
expect(response.status).toBe(200);
|
|
701
|
-
|
|
702
|
-
const data = await response.json();
|
|
703
|
-
console.log('Updated dist-tags after delete:', data);
|
|
704
|
-
|
|
705
|
-
expect(data).toHaveProperty('latest', '4.17.21');
|
|
706
|
-
expect(data).not.toHaveProperty('beta');
|
|
707
|
-
console.log('Successfully verified dist-tag remove');
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
it('should not delete the latest tag', async () => {
|
|
711
|
-
console.log('Testing dist-tag remove for latest tag');
|
|
712
|
-
const response = await mockFetch(`${BASE_URL}/-/package/lodash/dist-tags/latest`, {
|
|
713
|
-
method: 'DELETE',
|
|
714
|
-
});
|
|
715
|
-
expect(response.status).toBe(400);
|
|
716
|
-
|
|
717
|
-
const data = await response.json();
|
|
718
|
-
console.log('Response for deleting latest tag:', data);
|
|
719
|
-
|
|
720
|
-
expect(data).toHaveProperty('error', 'Cannot delete the "latest" tag');
|
|
721
|
-
console.log('Successfully verified protection of latest tag');
|
|
722
|
-
});
|
|
723
|
-
})
|
|
724
|
-
})
|
|
725
|
-
|
|
726
|
-
function createMockDb() {
|
|
727
|
-
// ... existing code ...
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const createMockDrizzle = (mockDb) => {
|
|
731
|
-
return mockDb;
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
// Create a mock bucket for R2
|
|
735
|
-
function createMockBucket() {
|
|
736
|
-
return {
|
|
737
|
-
put: async () => {},
|
|
738
|
-
get: async (key) => {
|
|
739
|
-
const parts = key.split('/');
|
|
740
|
-
const pkgName = parts[0];
|
|
741
|
-
const tarball = parts[1];
|
|
742
|
-
const versionMatch = tarball.match(new RegExp(`${pkgName}-(.*)\\.tgz`));
|
|
743
|
-
|
|
744
|
-
if (versionMatch && TEST_PACKAGES[pkgName] && TEST_PACKAGES[pkgName].versions[versionMatch[1]]) {
|
|
745
|
-
return {
|
|
746
|
-
body: new ReadableStream({
|
|
747
|
-
start(controller) {
|
|
748
|
-
controller.enqueue(new TextEncoder().encode('mock-tarball-content'));
|
|
749
|
-
controller.close();
|
|
750
|
-
}
|
|
751
|
-
})
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
return null;
|
|
755
|
-
}
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Update the app setup function
|
|
760
|
-
function createApp() {
|
|
761
|
-
const testDb = createMockDb();
|
|
762
|
-
const mockBucket = createMockBucket();
|
|
763
|
-
|
|
764
|
-
const app = new Hono();
|
|
765
|
-
|
|
766
|
-
// Set up middleware and context
|
|
767
|
-
app.use('*', async (c, next) => {
|
|
768
|
-
c.env = {
|
|
769
|
-
DB: testDb,
|
|
770
|
-
BUCKET: mockBucket
|
|
771
|
-
};
|
|
772
|
-
|
|
773
|
-
// Mock DB operations
|
|
774
|
-
c.db = {
|
|
775
|
-
getPackage: async (name) => {
|
|
776
|
-
const pkg = TEST_PACKAGES[name];
|
|
777
|
-
if (pkg) {
|
|
778
|
-
return {
|
|
779
|
-
name,
|
|
780
|
-
tags: pkg['dist-tags']
|
|
781
|
-
};
|
|
782
|
-
}
|
|
783
|
-
return null;
|
|
784
|
-
},
|
|
785
|
-
|
|
786
|
-
getVersion: async (spec) => {
|
|
787
|
-
const [name, version] = spec.split('@');
|
|
788
|
-
const pkg = TEST_PACKAGES[name];
|
|
789
|
-
if (pkg && pkg.versions && pkg.versions[version]) {
|
|
790
|
-
return {
|
|
791
|
-
spec,
|
|
792
|
-
manifest: pkg.versions[version],
|
|
793
|
-
published_at: new Date().toISOString()
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
return null;
|
|
797
|
-
},
|
|
798
|
-
|
|
799
|
-
getToken: async (token) => {
|
|
800
|
-
if (token === AUTH_TOKEN) {
|
|
801
|
-
return {
|
|
802
|
-
token,
|
|
803
|
-
uuid: 'admin',
|
|
804
|
-
scope: [
|
|
805
|
-
{
|
|
806
|
-
values: ['*'],
|
|
807
|
-
types: { pkg: { read: true, write: true } }
|
|
808
|
-
},
|
|
809
|
-
{
|
|
810
|
-
values: ['*'],
|
|
811
|
-
types: { user: { read: true, write: true } }
|
|
812
|
-
}
|
|
813
|
-
]
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
return null;
|
|
817
|
-
},
|
|
818
|
-
|
|
819
|
-
upsertPackage: async () => {},
|
|
820
|
-
upsertVersion: async () => {},
|
|
821
|
-
upsertToken: async () => {},
|
|
822
|
-
searchPackages: async () => []
|
|
823
|
-
};
|
|
824
|
-
|
|
825
|
-
await next();
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
// Add routes
|
|
829
|
-
app.get('/-/ping', (c) => c.json({ ok: true }));
|
|
830
|
-
|
|
831
|
-
// Package routes - simple implementations for test only
|
|
832
|
-
app.get('/:pkg', async (c) => {
|
|
833
|
-
const pkg = c.req.param('pkg');
|
|
834
|
-
console.log(`Packument request for: ${pkg}`);
|
|
835
|
-
|
|
836
|
-
if (TEST_PACKAGES[pkg]) {
|
|
837
|
-
const packageData = TEST_PACKAGES[pkg];
|
|
838
|
-
const versions = {};
|
|
839
|
-
|
|
840
|
-
Object.entries(packageData.versions).forEach(([version, versionData]) => {
|
|
841
|
-
versions[version] = versionData;
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
return c.json({
|
|
845
|
-
name: packageData.name,
|
|
846
|
-
'dist-tags': packageData['dist-tags'],
|
|
847
|
-
versions
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
return c.json({ error: 'Package not found' }, 404);
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
app.get('/:pkg/:version', async (c) => {
|
|
854
|
-
const pkg = c.req.param('pkg');
|
|
855
|
-
let version = c.req.param('version');
|
|
856
|
-
|
|
857
|
-
console.log(`Manifest request for: ${pkg}@${version}`);
|
|
858
|
-
|
|
859
|
-
if (TEST_PACKAGES[pkg]) {
|
|
860
|
-
const packageData = TEST_PACKAGES[pkg];
|
|
861
|
-
|
|
862
|
-
// Handle dist-tags
|
|
863
|
-
if (packageData['dist-tags'][version]) {
|
|
864
|
-
version = packageData['dist-tags'][version];
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
if (packageData.versions[version]) {
|
|
868
|
-
return c.json(packageData.versions[version]);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
return c.json({ error: 'Version not found' }, 404);
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
app.get('/:pkg/-/:tarball', async (c) => {
|
|
875
|
-
const pkg = c.req.param('pkg');
|
|
876
|
-
const tarball = c.req.param('tarball');
|
|
877
|
-
|
|
878
|
-
console.log(`Tarball request for: ${pkg}/${tarball}`);
|
|
879
|
-
|
|
880
|
-
const versionMatch = tarball.match(new RegExp(`${pkg}-(.*)\\.tgz`));
|
|
881
|
-
if (versionMatch && TEST_PACKAGES[pkg] && TEST_PACKAGES[pkg].versions[versionMatch[1]]) {
|
|
882
|
-
// Return a mock tarball
|
|
883
|
-
return new Response('mock-tarball-content', {
|
|
884
|
-
headers: {
|
|
885
|
-
'Content-Type': 'application/octet-stream',
|
|
886
|
-
'Cache-Control': 'public, max-age=31536000'
|
|
887
|
-
}
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
return c.json({ error: 'Tarball not found' }, 404);
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
app.get('/-/package/:pkg/dist-tags', getPackageDistTags);
|
|
894
|
-
app.get('/-/package/:pkg/dist-tags/:tag', getPackageDistTags);
|
|
895
|
-
app.put('/-/package/:pkg/dist-tags/:tag', putPackageDistTag);
|
|
896
|
-
app.delete('/-/package/:pkg/dist-tags/:tag', deletePackageDistTag);
|
|
897
|
-
|
|
898
|
-
app.get('/-/package/:scope%2f:pkg/dist-tags', getPackageDistTags);
|
|
899
|
-
app.get('/-/package/:scope%2f:pkg/dist-tags/:tag', getPackageDistTags);
|
|
900
|
-
app.put('/-/package/:scope%2f:pkg/dist-tags/:tag', putPackageDistTag);
|
|
901
|
-
app.delete('/-/package/:scope%2f:pkg/dist-tags/:tag', deletePackageDistTag);
|
|
902
|
-
|
|
903
|
-
return app;
|
|
904
|
-
}
|