@verdaccio/proxy 6.0.0-6-next.18 → 6.0.0-6-next.21

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.
@@ -0,0 +1,354 @@
1
+ import nock from 'nock';
2
+ import path from 'path';
3
+ import { setTimeout } from 'timers/promises';
4
+
5
+ import { Config, parseConfigFile } from '@verdaccio/config';
6
+ import { API_ERROR, errorUtils } from '@verdaccio/core';
7
+
8
+ import { ProxyStorage } from '../src';
9
+
10
+ const getConf = (name) => path.join(__dirname, '/conf', name);
11
+
12
+ const mockDebug = jest.fn();
13
+ const mockInfo = jest.fn();
14
+ const mockHttp = jest.fn();
15
+ const mockError = jest.fn();
16
+ const mockWarn = jest.fn();
17
+
18
+ // mock to get the headers fixed value
19
+ jest.mock('crypto', () => {
20
+ return {
21
+ randomBytes: (): { toString: () => string } => {
22
+ return {
23
+ toString: (): string => 'foo-random-bytes',
24
+ };
25
+ },
26
+ pseudoRandomBytes: (): { toString: () => string } => {
27
+ return {
28
+ toString: (): string => 'foo-phseudo-bytes',
29
+ };
30
+ },
31
+ };
32
+ });
33
+
34
+ jest.mock('@verdaccio/logger', () => {
35
+ const originalLogger = jest.requireActual('@verdaccio/logger');
36
+ return {
37
+ ...originalLogger,
38
+ logger: {
39
+ child: () => ({
40
+ debug: (arg, msg) => mockDebug(arg, msg),
41
+ info: (arg, msg) => mockInfo(arg, msg),
42
+ http: (arg, msg) => mockHttp(arg, msg),
43
+ error: (arg, msg) => mockError(arg, msg),
44
+ warn: (arg, msg) => mockWarn(arg, msg),
45
+ }),
46
+ },
47
+ };
48
+ });
49
+
50
+ const domain = 'https://registry.npmjs.org';
51
+
52
+ describe('proxy', () => {
53
+ beforeEach(() => {
54
+ nock.cleanAll();
55
+ });
56
+ const defaultRequestOptions = {
57
+ url: 'https://registry.npmjs.org',
58
+ };
59
+ const proxyPath = getConf('proxy1.yaml');
60
+ const conf = new Config(parseConfigFile(proxyPath));
61
+
62
+ describe('getRemoteMetadataNext', () => {
63
+ beforeEach(() => {
64
+ nock.cleanAll();
65
+ nock.abortPendingRequests();
66
+ jest.clearAllMocks();
67
+ });
68
+ describe('basic requests', () => {
69
+ test('success call to remote', async () => {
70
+ nock(domain, {
71
+ reqheaders: {
72
+ accept: 'application/json;',
73
+ 'accept-encoding': 'gzip',
74
+ 'x-forwarded-for': '127.0.0.1',
75
+ via: '1.1 foo-phseudo-bytes (Verdaccio)',
76
+ },
77
+ })
78
+ .get('/jquery')
79
+ .reply(200, { body: 'test' });
80
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
81
+ const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
82
+ remoteAddress: '127.0.0.1',
83
+ });
84
+ expect(manifest).toEqual({ body: 'test' });
85
+ });
86
+ });
87
+
88
+ describe('etag header', () => {
89
+ test('proxy call with etag', async () => {
90
+ nock(domain, {
91
+ reqheaders: {
92
+ accept: 'application/json;',
93
+ 'accept-encoding': 'gzip',
94
+ 'x-forwarded-for': '127.0.0.1',
95
+ via: '1.1 foo-phseudo-bytes (Verdaccio)',
96
+ },
97
+ })
98
+ .get('/jquery')
99
+ .reply(
100
+ 200,
101
+ { body: 'test' },
102
+ {
103
+ etag: () => `_ref_4444`,
104
+ }
105
+ );
106
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
107
+ const [manifest, etag] = await prox1.getRemoteMetadataNext('jquery', {
108
+ remoteAddress: '127.0.0.1',
109
+ });
110
+ expect(etag).toEqual('_ref_4444');
111
+ expect(manifest).toEqual({ body: 'test' });
112
+ });
113
+
114
+ test('proxy call with etag as option', async () => {
115
+ nock(domain, {
116
+ reqheaders: {
117
+ accept: 'application/json;',
118
+ 'accept-encoding': 'gzip',
119
+ 'x-forwarded-for': '127.0.0.1',
120
+ via: '1.1 foo-phseudo-bytes (Verdaccio)',
121
+ // match only if etag is set as option
122
+ 'if-none-match': 'foo',
123
+ },
124
+ })
125
+ .get('/jquery')
126
+ .reply(
127
+ 200,
128
+ { body: 'test' },
129
+ {
130
+ etag: () => `_ref_4444`,
131
+ }
132
+ );
133
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
134
+ const [manifest, etag] = await prox1.getRemoteMetadataNext('jquery', {
135
+ etag: 'foo',
136
+ remoteAddress: '127.0.0.1',
137
+ });
138
+ expect(etag).toEqual('_ref_4444');
139
+ expect(manifest).toEqual({ body: 'test' });
140
+ });
141
+ });
142
+
143
+ describe('log activity', () => {
144
+ test('proxy call with etag', async () => {
145
+ nock(domain)
146
+ .get('/jquery')
147
+ .reply(200, { body: { name: 'foo', version: '1.0.0' } }, {});
148
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
149
+ await prox1.getRemoteMetadataNext('jquery', {
150
+ remoteAddress: '127.0.0.1',
151
+ });
152
+ expect(mockHttp).toHaveBeenCalledTimes(2);
153
+ expect(mockHttp).toHaveBeenCalledWith(
154
+ {
155
+ request: { method: 'GET', url: `${domain}/jquery` },
156
+ status: 200,
157
+ },
158
+ "@{!status}, req: '@{request.method} @{request.url}' (streaming)"
159
+ );
160
+ expect(mockHttp).toHaveBeenLastCalledWith(
161
+ {
162
+ request: { method: 'GET', url: `${domain}/jquery` },
163
+ status: 200,
164
+ bytes: {
165
+ in: 0,
166
+ out: 41,
167
+ },
168
+ },
169
+ "@{!status}, req: '@{request.method} @{request.url}'"
170
+ );
171
+ });
172
+ });
173
+
174
+ describe('error handling', () => {
175
+ test('proxy call with 304', async () => {
176
+ nock(domain).get('/jquery').reply(304);
177
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
178
+ await expect(prox1.getRemoteMetadataNext('jquery', { etag: 'rev_3333' })).rejects.toThrow(
179
+ 'no data'
180
+ );
181
+ });
182
+
183
+ test('reply with error', async () => {
184
+ nock(domain).get('/jquery').replyWithError('something awful happened');
185
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
186
+ await expect(
187
+ prox1.getRemoteMetadataNext('jquery', {
188
+ remoteAddress: '127.0.0.1',
189
+ })
190
+ ).rejects.toThrowError(new Error('something awful happened'));
191
+ });
192
+
193
+ test('reply with 409 error', async () => {
194
+ nock(domain).get('/jquery').reply(409);
195
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
196
+ await expect(prox1.getRemoteMetadataNext('jquery', { retry: 0 })).rejects.toThrow(
197
+ new Error('bad status code: 409')
198
+ );
199
+ });
200
+
201
+ test('reply with bad body json format', async () => {
202
+ nock(domain).get('/jquery').reply(200, 'some-text');
203
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
204
+ await expect(
205
+ prox1.getRemoteMetadataNext('jquery', {
206
+ remoteAddress: '127.0.0.1',
207
+ })
208
+ ).rejects.toThrowError(
209
+ new Error(
210
+ 'Unexpected token s in JSON at position 0 in "https://registry.npmjs.org/jquery"'
211
+ )
212
+ );
213
+ });
214
+
215
+ test('400 error proxy call', async () => {
216
+ nock(domain).get('/jquery').reply(409);
217
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
218
+ await expect(
219
+ prox1.getRemoteMetadataNext('jquery', {
220
+ remoteAddress: '127.0.0.1',
221
+ })
222
+ ).rejects.toThrowError(
223
+ errorUtils.getInternalError(`${errorUtils.API_ERROR.BAD_STATUS_CODE}: 409`)
224
+ );
225
+ });
226
+
227
+ test('proxy not found', async () => {
228
+ nock(domain).get('/jquery').reply(404);
229
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
230
+ await expect(
231
+ prox1.getRemoteMetadataNext('jquery', {
232
+ remoteAddress: '127.0.0.1',
233
+ })
234
+ ).rejects.toThrowError(errorUtils.getNotFound(API_ERROR.NOT_PACKAGE_UPLINK));
235
+ expect(mockHttp).toHaveBeenCalledTimes(1);
236
+ expect(mockHttp).toHaveBeenLastCalledWith(
237
+ {
238
+ request: { method: 'GET', url: `${domain}/jquery` },
239
+ status: 404,
240
+ },
241
+ "@{!status}, req: '@{request.method} @{request.url}' (streaming)"
242
+ );
243
+ });
244
+ });
245
+
246
+ describe('retry', () => {
247
+ test('retry twice on 500 and return 200 logging offline activity', async () => {
248
+ nock(domain)
249
+ .get('/jquery')
250
+ .twice()
251
+ .reply(500, 'some-text')
252
+ .get('/jquery')
253
+ .once()
254
+ .reply(200, { body: { name: 'foo', version: '1.0.0' } });
255
+
256
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
257
+ const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
258
+ retry: { limit: 2 },
259
+ });
260
+ expect(manifest).toEqual({ body: { name: 'foo', version: '1.0.0' } });
261
+ expect(mockInfo).toHaveBeenCalledTimes(2);
262
+ expect(mockInfo).toHaveBeenLastCalledWith(
263
+ {
264
+ error: 'Response code 500 (Internal Server Error)',
265
+ request: { method: 'GET', url: `${domain}/jquery` },
266
+ retryCount: 2,
267
+ },
268
+ "retry @{retryCount} req: '@{request.method} @{request.url}'"
269
+ );
270
+ });
271
+
272
+ test('retry is exceded and uplink goes offline with logging activity', async () => {
273
+ nock(domain).get('/jquery').times(10).reply(500);
274
+
275
+ const prox1 = new ProxyStorage(defaultRequestOptions, conf);
276
+ await expect(
277
+ prox1.getRemoteMetadataNext('jquery', {
278
+ remoteAddress: '127.0.0.1',
279
+ retry: { limit: 2 },
280
+ })
281
+ ).rejects.toThrowError();
282
+ await expect(
283
+ prox1.getRemoteMetadataNext('jquery', {
284
+ remoteAddress: '127.0.0.1',
285
+ retry: { limit: 2 },
286
+ })
287
+ ).rejects.toThrowError(errorUtils.getInternalError(errorUtils.API_ERROR.UPLINK_OFFLINE));
288
+ expect(mockWarn).toHaveBeenCalledTimes(1);
289
+ expect(mockWarn).toHaveBeenLastCalledWith(
290
+ {
291
+ host: 'registry.npmjs.org',
292
+ },
293
+ 'host @{host} is now offline'
294
+ );
295
+ });
296
+
297
+ test('fails calls and recover with 200 with log online activity', async () => {
298
+ // This unit test is designed to verify if the uplink goes to offline
299
+ // and recover after the fail_timeout has expired.
300
+ nock(domain)
301
+ .get('/jquery')
302
+ .thrice()
303
+ .reply(500, 'some-text')
304
+ .get('/jquery')
305
+ .once()
306
+ .reply(200, { body: { name: 'foo', version: '1.0.0' } });
307
+
308
+ const prox1 = new ProxyStorage(
309
+ { ...defaultRequestOptions, fail_timeout: '1s', max_fails: 1 },
310
+ conf
311
+ );
312
+ // force retry
313
+ await expect(
314
+ prox1.getRemoteMetadataNext('jquery', {
315
+ remoteAddress: '127.0.0.1',
316
+ retry: { limit: 2 },
317
+ })
318
+ ).rejects.toThrowError();
319
+ // display offline error on exausted retry
320
+ await expect(
321
+ prox1.getRemoteMetadataNext('jquery', {
322
+ remoteAddress: '127.0.0.1',
323
+ retry: { limit: 2 },
324
+ })
325
+ ).rejects.toThrowError(errorUtils.getInternalError(errorUtils.API_ERROR.UPLINK_OFFLINE));
326
+ expect(mockWarn).toHaveBeenCalledTimes(2);
327
+ expect(mockWarn).toHaveBeenLastCalledWith(
328
+ {
329
+ host: 'registry.npmjs.org',
330
+ },
331
+ 'host @{host} is now offline'
332
+ );
333
+ expect(mockWarn).toHaveBeenLastCalledWith(
334
+ {
335
+ host: 'registry.npmjs.org',
336
+ },
337
+ 'host @{host} is now offline'
338
+ );
339
+ // this is based on max_fails, if change that also change here acordingly
340
+ await setTimeout(3000);
341
+ const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
342
+ retry: { limit: 2 },
343
+ });
344
+ expect(manifest).toEqual({ body: { name: 'foo', version: '1.0.0' } });
345
+ expect(mockWarn).toHaveBeenLastCalledWith(
346
+ {
347
+ host: 'registry.npmjs.org',
348
+ },
349
+ 'host @{host} is now online'
350
+ );
351
+ }, 10000);
352
+ });
353
+ });
354
+ });
@@ -3,20 +3,16 @@
3
3
  /* global AbortController */
4
4
  import getStream from 'get-stream';
5
5
  import path from 'path';
6
- import semver from 'semver';
6
+ import { MockAgent, setGlobalDispatcher } from 'undici';
7
7
 
8
8
  import { Config, parseConfigFile } from '@verdaccio/config';
9
9
  import { streamUtils } from '@verdaccio/core';
10
10
 
11
- import { ProxyStorage } from '../src/up-storage';
12
-
13
- // FUTURE: remove me when v15 is the min required version
14
- if (semver.lte(process.version, 'v15.0.0')) {
15
- global.AbortController = require('abortcontroller-polyfill/dist/cjs-ponyfill').AbortController;
16
- }
11
+ import { ProxyStorage } from '../src';
17
12
 
18
13
  const getConf = (name) => path.join(__dirname, '/conf', name);
19
14
 
15
+ // TODO: we can mock this globally maybe
20
16
  const mockDebug = jest.fn();
21
17
  const mockInfo = jest.fn();
22
18
  const mockHttp = jest.fn();
@@ -38,8 +34,6 @@ jest.mock('@verdaccio/logger', () => {
38
34
  };
39
35
  });
40
36
 
41
- const { MockAgent } = require('undici');
42
- const { setGlobalDispatcher } = require('undici-fetch');
43
37
  const domain = 'https://registry.npmjs.org';
44
38
 
45
39
  describe('proxy', () => {
@@ -56,7 +50,7 @@ describe('proxy', () => {
56
50
  };
57
51
 
58
52
  describe('search', () => {
59
- test('get response from v1 endpoint', async () => {
53
+ test('get response from endpoint', async () => {
60
54
  const response = require('./partials/search-v1.json');
61
55
  const mockAgent = new MockAgent({ connections: 1 });
62
56
  mockAgent.disableNetConnect();
@@ -90,13 +84,13 @@ describe('proxy', () => {
90
84
  ).rejects.toThrow('bad status code 409 from uplink');
91
85
  });
92
86
 
93
- test.todo('abort search from v1 endpoint');
87
+ test.todo('abort search from endpoint');
94
88
 
95
89
  // TODO: we should test the gzip deflate here, but is hard to test
96
90
  // fix me if you can deal with Incorrect Header Check issue
97
- test.todo('get file from v1 endpoint with gzip headers');
91
+ test.todo('get file from endpoint with gzip headers');
98
92
 
99
- test('search v1 endpoint fails', async () => {
93
+ test('search endpoint fails', async () => {
100
94
  const mockAgent = new MockAgent({ connections: 1 });
101
95
  mockAgent.disableNetConnect();
102
96
  setGlobalDispatcher(mockAgent);