@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.
@@ -1,298 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
-
3
- // Mock config
4
- const CONFIG = {
5
- DOMAIN: 'registry.npmjs.org',
6
- PROXY_PACKAGES: ['fastify', 'hono']
7
- };
8
-
9
- /**
10
- * This test demonstrates testing an actual route handler that uses
11
- * the Hono context and waitUntil, with proper mocking of both.
12
- */
13
- describe('Route Handler with waitUntil', () => {
14
- /**
15
- * Creates a mock Hono context with necessary properties
16
- */
17
- function createMockContext(options = {}) {
18
- // Track background task functions
19
- const backgroundTaskFns = [];
20
- let responseData = null;
21
- let responseStatus = 200;
22
- let responseHeaders = {};
23
- let wasJsonCalled = false;
24
-
25
- // Default options
26
- const defaults = {
27
- params: { packageName: 'test-package' },
28
- fresh: false,
29
- cacheData: null,
30
- upstreamData: { name: 'test-package', version: '1.0.0' },
31
- upstreamError: null
32
- };
33
-
34
- const opts = { ...defaults, ...options };
35
-
36
- // Mock fetch function
37
- const mockFetch = vi.fn().mockImplementation(async (url) => {
38
- if (opts.upstreamError) {
39
- throw opts.upstreamError;
40
- }
41
- return {
42
- json: async () => opts.upstreamData,
43
- status: 200,
44
- ok: true
45
- };
46
- });
47
-
48
- // Mock DB
49
- const mockDb = {
50
- get: vi.fn().mockImplementation(async (key) => {
51
- if (!opts.cacheData) return null;
52
-
53
- return {
54
- value: opts.cacheData,
55
- timestamp: opts.fresh
56
- ? Date.now() - 60000 // 1 minute old (fresh)
57
- : Date.now() - 3600000 // 1 hour old (stale)
58
- };
59
- }),
60
- set: vi.fn()
61
- };
62
-
63
- // Create context
64
- return {
65
- // Request info
66
- req: {
67
- url: `https://registry.npmjs.org/${opts.params.packageName}`,
68
- fresh: opts.fresh
69
- },
70
-
71
- // Route parameters
72
- params: opts.params,
73
-
74
- // Database methods
75
- db: mockDb,
76
-
77
- // Response methods
78
- status: vi.fn((code) => {
79
- responseStatus = code;
80
- return context;
81
- }),
82
-
83
- header: vi.fn((name, value) => {
84
- responseHeaders[name] = value;
85
- return context;
86
- }),
87
-
88
- json: vi.fn((data) => {
89
- responseData = data;
90
- wasJsonCalled = true;
91
- return context;
92
- }),
93
-
94
- // Execution context for background tasks
95
- executionCtx: {
96
- waitUntil: vi.fn((promise) => {
97
- // Instead of accepting the promise directly, we'll store a function
98
- // that will return the promise when called
99
- const promiseCreator = () => promise;
100
- backgroundTaskFns.push(promiseCreator);
101
- })
102
- },
103
-
104
- // Utility methods for testing
105
- runBackgroundTasks: async () => {
106
- const tasks = [...backgroundTaskFns];
107
- backgroundTaskFns.length = 0;
108
-
109
- for (const taskFn of tasks) {
110
- try {
111
- await taskFn();
112
- } catch (error) {
113
- console.error('Background task error:', error);
114
- }
115
- }
116
- },
117
-
118
- getResponse: () => ({
119
- data: responseData,
120
- status: responseStatus,
121
- headers: responseHeaders,
122
- wasJsonCalled
123
- }),
124
-
125
- // Expose methods for testing
126
- fetch: mockFetch,
127
- backgroundTaskFns
128
- };
129
- }
130
-
131
- // The context needs to be accessible in tests
132
- let context;
133
-
134
- /**
135
- * This is a simplified version of getPackageMetadata that demonstrates
136
- * the stale-while-revalidate caching pattern
137
- */
138
- async function getPackageMetadata(c) {
139
- const { packageName } = c.params;
140
-
141
- try {
142
- // Try to get from cache first
143
- const cachedData = await c.db.get(`package:${packageName}`);
144
-
145
- if (cachedData) {
146
- const isStale = Date.now() - cachedData.timestamp > 5 * 60 * 1000;
147
-
148
- if (!isStale) {
149
- // Return fresh cached data
150
- return c.json(cachedData.value);
151
- }
152
-
153
- // Return stale data, but refresh in background
154
- c.executionCtx.waitUntil(new Promise(async (resolve) => {
155
- // In a real implementation this executes immediately
156
- // In our test, we'll capture the function and run it later
157
- const bgRefresh = async () => {
158
- try {
159
- const response = await c.fetch(`https://${CONFIG.DOMAIN}/${packageName}`);
160
- const freshData = await response.json();
161
-
162
- await c.db.set(`package:${packageName}`, {
163
- value: freshData,
164
- timestamp: Date.now()
165
- });
166
-
167
- resolve();
168
- } catch (error) {
169
- console.error(`Error refreshing ${packageName}:`, error);
170
- resolve();
171
- }
172
- };
173
-
174
- // Don't call bgRefresh() directly, we'll run it during runBackgroundTasks
175
- setTimeout(bgRefresh, 0);
176
- }));
177
-
178
- // Return stale data immediately
179
- return c.json(cachedData.value);
180
- }
181
-
182
- // Not in cache, fetch directly
183
- const response = await c.fetch(`https://${CONFIG.DOMAIN}/${packageName}`);
184
- const packageData = await response.json();
185
-
186
- // Store in cache
187
- await c.db.set(`package:${packageName}`, {
188
- value: packageData,
189
- timestamp: Date.now()
190
- });
191
-
192
- return c.json(packageData);
193
- } catch (error) {
194
- // Handle upstream errors gracefully
195
- console.error(`Error fetching ${packageName}:`, error);
196
- return c.status(500).json({
197
- error: `Failed to fetch package: ${packageName}`,
198
- message: error.message
199
- });
200
- }
201
- }
202
-
203
- describe('getPackageMetadata', () => {
204
- beforeEach(() => {
205
- vi.clearAllMocks();
206
- });
207
-
208
- it('should return cached data if fresh', async () => {
209
- // Create mock context with fresh cached data
210
- context = createMockContext({
211
- fresh: true,
212
- cacheData: { name: 'test-package', version: '1.0.0' }
213
- });
214
-
215
- // Execute the handler
216
- await getPackageMetadata(context);
217
-
218
- const { data } = context.getResponse();
219
-
220
- // Verify
221
- expect(context.db.get).toHaveBeenCalledWith('package:test-package');
222
- expect(context.fetch).not.toHaveBeenCalled();
223
- expect(data).toEqual({ name: 'test-package', version: '1.0.0' });
224
- expect(context.backgroundTaskFns.length).toBe(0);
225
- });
226
-
227
- it('should return stale data and refresh in background', async () => {
228
- // Setup context with stale data
229
- context = createMockContext({
230
- fresh: false,
231
- cacheData: { name: 'test-package', version: '1.0.0' },
232
- upstreamData: { name: 'test-package', version: '1.0.1' }
233
- });
234
-
235
- // Execute the handler
236
- await getPackageMetadata(context);
237
-
238
- const { data } = context.getResponse();
239
-
240
- // Verify initial response
241
- expect(context.db.get).toHaveBeenCalledWith('package:test-package');
242
- expect(context.fetch).not.toHaveBeenCalled(); // Not yet called
243
- expect(data).toEqual({ name: 'test-package', version: '1.0.0' });
244
- expect(context.backgroundTaskFns.length).toBe(1);
245
-
246
- // Now run background tasks
247
- await context.runBackgroundTasks();
248
-
249
- // Verify background refresh
250
- expect(context.fetch).toHaveBeenCalledWith('https://registry.npmjs.org/test-package');
251
- expect(context.db.set).toHaveBeenCalledWith('package:test-package', {
252
- value: { name: 'test-package', version: '1.0.1' },
253
- timestamp: expect.any(Number)
254
- });
255
- });
256
-
257
- it('should fetch directly if not in cache', async () => {
258
- // Setup context with no cached data
259
- context = createMockContext({
260
- cacheData: null,
261
- upstreamData: { name: 'test-package', version: '1.0.1' }
262
- });
263
-
264
- // Execute the handler
265
- await getPackageMetadata(context);
266
-
267
- const { data } = context.getResponse();
268
-
269
- // Verify
270
- expect(context.db.get).toHaveBeenCalledWith('package:test-package');
271
- expect(context.fetch).toHaveBeenCalledWith('https://registry.npmjs.org/test-package');
272
- expect(data).toEqual({ name: 'test-package', version: '1.0.1' });
273
- expect(context.backgroundTaskFns.length).toBe(0);
274
- });
275
-
276
- it('should handle upstream errors gracefully', async () => {
277
- // Setup context with error
278
- context = createMockContext({
279
- cacheData: null,
280
- upstreamError: new Error('Network error')
281
- });
282
-
283
- // Execute the handler
284
- await getPackageMetadata(context);
285
-
286
- const { data, status } = context.getResponse();
287
-
288
- // Verify
289
- expect(context.db.get).toHaveBeenCalledWith('package:test-package');
290
- expect(context.fetch).toThrow;
291
- expect(status).toBe(500);
292
- expect(data).toEqual({
293
- error: 'Failed to fetch package: test-package',
294
- message: 'Network error'
295
- });
296
- });
297
- });
298
- });
package/test/run-tests.js DELETED
@@ -1,151 +0,0 @@
1
- const { spawn } = require('child_process')
2
- const { execSync } = require('child_process')
3
- const { setTimeout } = require('timers')
4
-
5
- function sleep(ms) {
6
- return new Promise(resolve => {
7
- setTimeout(resolve, ms)
8
- })
9
- }
10
-
11
- async function waitForServer(url, maxAttempts = 5) {
12
- for (let i = 0; i < maxAttempts; i++) {
13
- try {
14
- const response = await fetch(url, {
15
- headers: {
16
- 'User-Agent': 'vlt-serverless-registry-test'
17
- }
18
- })
19
- if (response.ok) {
20
- console.log('Server is ready!')
21
- return true
22
- }
23
- } catch (err) {
24
- console.log(`Waiting for server to start... (attempt ${i + 1}/${maxAttempts})`)
25
- await new Promise(resolve => setTimeout(resolve, 2000)) // Wait 2 seconds between attempts
26
- }
27
- }
28
- throw new Error('Server failed to start')
29
- }
30
-
31
- async function runTests() {
32
- let wrangler = null
33
- let vitest = null
34
- let testTimeout = null
35
-
36
- // Function to clean up processes
37
- const cleanup = async () => {
38
- console.log('Cleaning up processes...')
39
- if (testTimeout) {
40
- clearTimeout(testTimeout)
41
- testTimeout = null
42
- }
43
- if (wrangler) {
44
- wrangler.kill()
45
- wrangler = null
46
- }
47
- if (vitest) {
48
- vitest.kill()
49
- vitest = null
50
- }
51
- }
52
-
53
- // Handle process termination
54
- const handleTermination = async () => {
55
- console.log('Received termination signal, cleaning up...')
56
- await cleanup()
57
- process.exit(0)
58
- }
59
-
60
- process.on('SIGINT', handleTermination)
61
- process.on('SIGTERM', handleTermination)
62
-
63
- try {
64
- // Kill any existing wrangler processes
65
- try {
66
- execSync('pkill -f wrangler', { stdio: 'ignore' })
67
- } catch (err) {
68
- // Ignore errors if no process was found
69
- }
70
-
71
- // Setup test database
72
- console.log('Setting up test database...')
73
- execSync('npm run test:setup', { stdio: 'inherit' })
74
-
75
- // Start wrangler dev server
76
- console.log('Starting wrangler dev server...')
77
- wrangler = spawn('wrangler', ['dev', '--local', '--persist-to', 'local-store', '--experimental-json-config'], {
78
- stdio: ['pipe', 'pipe', 'pipe']
79
- })
80
-
81
- let serverStarted = false
82
- let serverError = null
83
-
84
- // Handle server output
85
- wrangler.stdout.on('data', (data) => {
86
- const output = data.toString().trim()
87
- console.log(`[Wrangler] ${output}`)
88
-
89
- // Check for server ready message
90
- if (output.includes('Ready in') || output.includes('Listening on')) {
91
- serverStarted = true
92
- }
93
- })
94
-
95
- wrangler.stderr.on('data', (data) => {
96
- const output = data.toString().trim()
97
- console.error(`[Wrangler Error] ${output}`)
98
-
99
- // Check for common errors
100
- if (output.includes('Error') || output.includes('Failed')) {
101
- serverError = output
102
- }
103
- })
104
-
105
- // Wait for server to start
106
- try {
107
- await waitForServer('http://localhost:1337')
108
- } catch (err) {
109
- console.error('Failed to start server:', err)
110
- if (serverError) {
111
- console.error('Server error:', serverError)
112
- }
113
- await cleanup()
114
- process.exit(1)
115
- }
116
-
117
- // Run vitest
118
- console.log('Running tests...')
119
- vitest = spawn('vitest', ['run', '--no-watch'], {
120
- stdio: 'inherit',
121
- env: {
122
- ...process.env,
123
- NODE_ENV: 'test',
124
- VITEST_MAX_TIMEOUT: '120000' // 2 minutes
125
- }
126
- })
127
-
128
- // Set a timeout for the entire test process
129
- testTimeout = setTimeout(() => {
130
- console.error('Tests timed out after 120 seconds')
131
- cleanup().then(() => process.exit(1))
132
- }, 120000)
133
-
134
- // Wait for vitest to complete
135
- vitest.on('close', async (code) => {
136
- console.log('Tests completed')
137
- await cleanup()
138
- process.exit(code)
139
- })
140
-
141
- } catch (err) {
142
- console.error('Test runner failed:', err)
143
- await cleanup()
144
- process.exit(1)
145
- }
146
- }
147
-
148
- runTests().catch((err) => {
149
- console.error('Test runner failed:', err)
150
- process.exit(1)
151
- })
@@ -1,190 +0,0 @@
1
- import { vi, afterAll } from 'vitest';
2
-
3
- // Setup mock global objects for testing
4
- // This runs before the tests to provide necessary mocks
5
-
6
- // Mock global web standard objects that are available in Cloudflare Workers
7
- // but not in Node.js test environment
8
-
9
- // Mock Request class if not available
10
- if (typeof Request === 'undefined') {
11
- global.Request = class Request {
12
- constructor(input, init) {
13
- this.url = input;
14
- this.method = (init?.method || 'GET').toUpperCase();
15
- this.headers = new Headers(init?.headers);
16
- this.body = init?.body;
17
- this.json = async () => {
18
- if (typeof this.body === 'string') {
19
- return JSON.parse(this.body);
20
- } else if (this.body && typeof this.body.json === 'function') {
21
- return this.body.json();
22
- }
23
- return undefined;
24
- };
25
- this.text = async () => {
26
- if (typeof this.body === 'string') {
27
- return this.body;
28
- } else if (this.body && typeof this.body.text === 'function') {
29
- return this.body.text();
30
- }
31
- return '';
32
- };
33
- this.arrayBuffer = async () => new ArrayBuffer(0);
34
- this.formData = async () => new FormData();
35
- this.blob = async () => new Blob();
36
- }
37
-
38
- get path() {
39
- try {
40
- return new URL(this.url).pathname;
41
- } catch (e) {
42
- return this.url;
43
- }
44
- }
45
- };
46
- }
47
-
48
- // Mock Response class if not available
49
- if (typeof Response === 'undefined') {
50
- global.Response = class Response {
51
- constructor(body, init) {
52
- this.body = body;
53
- this.status = init?.status || 200;
54
- this.statusText = init?.statusText || '';
55
- this.headers = new Headers(init?.headers);
56
- this.ok = this.status >= 200 && this.status < 300;
57
- this.type = 'default';
58
- this.redirected = false;
59
- this.url = '';
60
- this._bodyUsed = false;
61
- }
62
-
63
- get bodyUsed() {
64
- return this._bodyUsed;
65
- }
66
-
67
- async arrayBuffer() {
68
- this._bodyUsed = true;
69
- if (this.body instanceof ArrayBuffer) return this.body;
70
- if (typeof this.body === 'string') {
71
- const encoder = new TextEncoder();
72
- return encoder.encode(this.body).buffer;
73
- }
74
- return new ArrayBuffer(0);
75
- }
76
-
77
- async text() {
78
- this._bodyUsed = true;
79
- if (typeof this.body === 'string') return this.body;
80
- if (this.body && typeof this.body.text === 'function') return this.body.text();
81
- if (this.body && typeof this.body === 'object') return JSON.stringify(this.body);
82
- return '';
83
- }
84
-
85
- async json() {
86
- this._bodyUsed = true;
87
- if (typeof this.body === 'string') return JSON.parse(this.body);
88
- if (this.body && typeof this.body.json === 'function') return this.body.json();
89
- if (this.body && typeof this.body === 'object') return this.body;
90
- return {};
91
- }
92
-
93
- clone() {
94
- return new Response(this.body, {
95
- status: this.status,
96
- statusText: this.statusText,
97
- headers: new Headers(this.headers)
98
- });
99
- }
100
- };
101
- }
102
-
103
- // Mock Headers class if not available
104
- if (typeof Headers === 'undefined') {
105
- global.Headers = class Headers {
106
- constructor(init) {
107
- this._headers = new Map();
108
- if (init) {
109
- if (init instanceof Headers) {
110
- init.forEach((value, key) => {
111
- this.append(key, value);
112
- });
113
- } else if (Array.isArray(init)) {
114
- init.forEach(([key, value]) => {
115
- this.append(key, value);
116
- });
117
- } else if (typeof init === 'object') {
118
- Object.entries(init).forEach(([key, value]) => {
119
- this.append(key, value);
120
- });
121
- }
122
- }
123
- }
124
-
125
- append(name, value) {
126
- name = name.toLowerCase();
127
- if (this._headers.has(name)) {
128
- this._headers.set(name, `${this._headers.get(name)}, ${value}`);
129
- } else {
130
- this._headers.set(name, value);
131
- }
132
- }
133
-
134
- delete(name) {
135
- this._headers.delete(name.toLowerCase());
136
- }
137
-
138
- get(name) {
139
- return this._headers.get(name.toLowerCase()) || null;
140
- }
141
-
142
- has(name) {
143
- return this._headers.has(name.toLowerCase());
144
- }
145
-
146
- set(name, value) {
147
- this._headers.set(name.toLowerCase(), value);
148
- }
149
-
150
- forEach(callback) {
151
- this._headers.forEach((value, key) => {
152
- callback(value, key, this);
153
- });
154
- }
155
- };
156
- }
157
-
158
- // Mock TransformStream if not available
159
- if (typeof TransformStream === 'undefined') {
160
- global.TransformStream = class TransformStream {
161
- constructor() {
162
- this.readable = {
163
- getReader: () => ({
164
- read: async () => ({ done: true, value: undefined }),
165
- cancel: async () => {}
166
- })
167
- };
168
- this.writable = {
169
- getWriter: () => ({
170
- write: async () => {},
171
- close: async () => {},
172
- abort: async () => {}
173
- })
174
- };
175
- }
176
- };
177
- }
178
-
179
- // Mock console to avoid noise in tests
180
- const originalConsole = { ...console };
181
- console.debug = vi.fn();
182
- console.log = vi.fn();
183
- console.info = vi.fn();
184
- // Keep error and warn for debugging
185
-
186
- // Clean up all mocks after tests are done
187
- afterAll(() => {
188
- vi.restoreAllMocks();
189
- Object.assign(console, originalConsole);
190
- });
package/test/setup.js DELETED
@@ -1,64 +0,0 @@
1
- import { beforeAll, afterAll } from 'vitest'
2
- import { execSync } from 'child_process'
3
-
4
- // Mock environment variables needed for testing
5
- beforeAll(() => {
6
- process.env.DOMAIN = 'http://localhost:0'
7
- process.env.PROXY = 'true'
8
- process.env.PROXY_URL = 'https://registry.npmjs.org'
9
-
10
- // Setup test database
11
- try {
12
- execSync('npm run test:setup', { stdio: 'inherit' })
13
- } catch (err) {
14
- console.error('Failed to setup test database:', err)
15
- process.exit(1)
16
- }
17
-
18
- // Mock the Cloudflare Workers environment
19
- globalThis.env = {
20
- DB: {
21
- prepare: async (query) => ({
22
- bind: async (...args) => ({
23
- run: async () => {
24
- // For token validation
25
- if (query.includes('SELECT * FROM tokens')) {
26
- return {
27
- results: [{
28
- token: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
29
- uuid: 'admin',
30
- scope: JSON.stringify([
31
- {
32
- values: ['*'],
33
- types: {
34
- pkg: { read: true, write: true },
35
- user: { read: true, write: true }
36
- }
37
- }
38
- ])
39
- }]
40
- }
41
- }
42
- return { results: [] }
43
- }
44
- })
45
- })
46
- },
47
- BUCKET: {
48
- get: async () => null,
49
- put: async () => {}
50
- },
51
- executionCtx: {
52
- waitUntil: (promise) => promise
53
- }
54
- }
55
- })
56
-
57
- afterAll(() => {
58
- // Cleanup test database
59
- try {
60
- execSync('npm run test:cleanup', { stdio: 'inherit' })
61
- } catch (err) {
62
- console.error('Failed to cleanup test database:', err)
63
- }
64
- })