import _ from 'lodash'; import nock from 'nock'; import { Config, UpLinkConf } from '@verdaccio/types'; import AppConfig from '../../../../src/lib/config'; import { API_ERROR, ERROR_CODE, HTTP_STATUS, TOKEN_BASIC, TOKEN_BEARER, } from '../../../../src/lib/constants'; import { setup } from '../../../../src/lib/logger'; import ProxyStorage from '../../../../src/lib/up-storage'; import { DOMAIN_SERVERS } from '../../../functional/config.functional'; import { mockServer } from '../../__helper/mock'; import configExample from '../../partials/config'; setup({}); describe('UpStorage', () => { const mockServerPort = 55547; let mockRegistry; const uplinkDefault = { url: `http://localhost:${mockServerPort}`, }; const generateProxy = (config: UpLinkConf = uplinkDefault) => { const appConfig: Config = new AppConfig(configExample()); return new ProxyStorage(config, appConfig); }; beforeAll(async () => { mockRegistry = await mockServer(mockServerPort).init(); }); beforeEach(() => { nock.cleanAll(); }); afterAll(function (done) { mockRegistry[0].stop(); done(); }); test('should be defined', () => { const proxy = generateProxy(); expect(proxy).toBeDefined(); }); describe('getRemoteMetadata', () => { beforeEach(() => { // @ts-ignore process.env.TOKEN_TEST_ENV = 'foo'; // @ts-ignore process.env.NPM_TOKEN = 'foo'; }); afterEach(() => { delete process.env.TOKEN_TEST_ENV; delete process.env.NPM_TOKEN; }); test('should be get remote metadata', (done) => { const proxy = generateProxy(); proxy.getRemoteMetadata('jquery', {}, (err, data, etag) => { expect(err).toBeNull(); expect(_.isString(etag)).toBeTruthy(); expect(data.name).toBe('jquery'); done(); }); }); test('should handle 404 on be get remote metadata', (done) => { nock('http://localhost:55547').get(`/jquery`).once().reply(404, { name: 'jquery' }); const proxy = generateProxy(); proxy.getRemoteMetadata('jquery', {}, (err) => { expect(err).not.toBeNull(); expect(err.message).toMatch(/package does not exist on uplink/); done(); }); }); test('should handle 500 on be get remote metadata', (done) => { nock('http://localhost:55547').get(`/jquery`).once().reply(500, { name: 'jquery' }); const proxy = generateProxy(); proxy.getRemoteMetadata('jquery', {}, (err) => { expect(err).not.toBeNull(); expect(err.message).toMatch(/bad status code: 500/); done(); }); }); test('should be get remote metadata with etag', (done) => { const proxy = generateProxy(); proxy.getRemoteMetadata('jquery', { etag: '123456' }, (err, data, etag) => { expect(err).toBeNull(); expect(_.isString(etag)).toBeTruthy(); expect(data.name).toBe('jquery'); done(); }); }); test('should be get remote metadata package does not exist', (done) => { const proxy = generateProxy(); proxy.getRemoteMetadata('@verdaccio/fake-package', { etag: '123456' }, (err) => { expect(err).not.toBeNull(); expect(err.statusCode).toBe(HTTP_STATUS.NOT_FOUND); expect(err.message).toMatch(API_ERROR.NOT_PACKAGE_UPLINK); done(); }); }); test('should be get remote metadata with json when uplink is npmmirror', (done) => { nock('https://registry.npmmirror.com').get(`/jquery`).reply(200, { name: 'jquery' }); const proxy = generateProxy({ url: 'https://registry.npmmirror.com' }); proxy.getRemoteMetadata('jquery', { json: true }, (err, data) => { expect(err).toBeNull(); expect(data.name).toBe('jquery'); done(); }); }); test('should be get remote metadata with auth header bearer', (done) => { nock('https://registry.npmmirror.com', { reqheaders: { authorization: 'Bearer foo', }, }) .get(`/jquery`) .reply(200, { name: 'jquery' }); const proxy = generateProxy({ url: 'https://registry.npmmirror.com', auth: { type: TOKEN_BEARER, token: 'foo', }, }); proxy.getRemoteMetadata('jquery', {}, (err, data, etag) => { expect(err).toBeNull(); // expect(_.isString(etag)).toBeTruthy(); expect(data.name).toBe('jquery'); done(); }); }); test('should be get remote metadata with auth node env TOKEN_TEST_ENV header bearer', (done) => { nock('https://registry.npmmirror.com', { reqheaders: { authorization: 'Bearer foo', }, }) .get(`/jquery`) .reply(200, { name: 'jquery' }); const proxy = generateProxy({ url: 'https://registry.npmmirror.com', auth: { type: TOKEN_BEARER, token_env: 'TOKEN_TEST_ENV', }, }); proxy.getRemoteMetadata('jquery', {}, (err, data, etag) => { expect(err).toBeNull(); expect(data.name).toBe('jquery'); done(); }); }); test('should be get remote metadata with auth node env NPM_TOKEN header bearer', (done) => { nock('https://registry.npmmirror.com', { reqheaders: { authorization: 'Bearer foo', }, }) .get(`/jquery`) .reply(200, { name: 'jquery' }); const proxy = generateProxy({ url: 'https://registry.npmmirror.com', auth: { type: TOKEN_BEARER, token_env: true, }, }); proxy.getRemoteMetadata('jquery', {}, (err, data, etag) => { expect(err).toBeNull(); expect(data.name).toBe('jquery'); done(); }); }); test('should be get remote metadata with auth header basic', (done) => { nock('https://registry.npmmirror.com', { reqheaders: { authorization: 'Basic foo', }, }) .get(`/jquery`) .reply(200, { name: 'jquery' }); const proxy = generateProxy({ url: 'https://registry.npmmirror.com', auth: { type: TOKEN_BASIC, token: 'foo', }, }); proxy.getRemoteMetadata('jquery', {}, (err, data, etag) => { expect(err).toBeNull(); // expect(_.isString(etag)).toBeTruthy(); expect(data.name).toBe('jquery'); done(); }); }); }); describe('error handling', () => { test('should fails if auth type is missing', () => { const proxy = generateProxy({ url: 'https://registry.npmmirror.com', auth: { type: TOKEN_BASIC, token: undefined, }, }); expect(function () { proxy.getRemoteMetadata('jquery', {}, () => {}); }).toThrow(/token is required/); }); test('should fails if token_env is undefined', () => { const proxy = generateProxy({ url: 'https://registry.npmmirror.com', auth: { type: TOKEN_BASIC, token_env: undefined, }, }); expect(function () { proxy.getRemoteMetadata('jquery', {}, () => {}); }).toThrow(ERROR_CODE.token_required); }); test('should fails if token_env is false', () => { const proxy = generateProxy({ url: 'https://registry.npmmirror.com', auth: { type: TOKEN_BASIC, token_env: false, }, }); expect(function () { proxy.getRemoteMetadata('jquery', {}, () => {}); }).toThrow(ERROR_CODE.token_required); }); test.skip('should fails if invalid token type', () => { const proxy = generateProxy({ url: 'https://registry.npmmirror.com', auth: { token: 'SomethingWrong', }, }); expect(function () { proxy.getRemoteMetadata('jquery', {}, () => {}); }).toThrow(ERROR_CODE.token_required); }); }); describe('fetchTarball', () => { test.skip('should fetch a tarball from uplink', (done) => { const proxy = generateProxy(); const tarball = `http://${DOMAIN_SERVERS}:${mockServerPort}/jquery/-/jquery-1.5.1.tgz`; const stream = proxy.fetchTarball(tarball); stream.on('error', function (err) { expect(err).toBeNull(); done(); }); stream.on('content-length', function (contentLength) { expect(contentLength).toBeDefined(); done(); }); }); test('should throw a 404 on fetch a tarball from uplink', (done) => { const proxy = generateProxy(); const tarball = `http://${DOMAIN_SERVERS}:${mockServerPort}/jquery/-/no-exist-1.5.1.tgz`; const stream = proxy.fetchTarball(tarball); stream.on('error', function (err: any) { expect(err).not.toBeNull(); expect(err.statusCode).toBe(HTTP_STATUS.NOT_FOUND); expect(err.message).toMatch(API_ERROR.NOT_FILE_UPLINK); done(); }); stream.on('content-length', function (contentLength) { expect(contentLength).toBeDefined(); done(); }); }); test('should be offline uplink', (done) => { const proxy = generateProxy(); const tarball = 'http://404.verdaccioo.com'; const stream = proxy.fetchTarball(tarball); expect(proxy.failed_requests).toBe(0); // to test a uplink is offline we have to be try 3 times // the default failed request are set to 2 process.nextTick(function () { stream.on('error', function (err) { expect(err).not.toBeNull(); // expect(err.statusCode).toBe(404); expect(proxy.failed_requests).toBe(1); const streamSecondTry = proxy.fetchTarball(tarball); streamSecondTry.on('error', function (err) { expect(err).not.toBeNull(); /* code: 'ENOTFOUND', errno: 'ENOTFOUND', */ // expect(err.statusCode).toBe(404); expect(proxy.failed_requests).toBe(2); const streamThirdTry = proxy.fetchTarball(tarball); streamThirdTry.on('error', function (err: any) { expect(err).not.toBeNull(); expect(err.statusCode).toBe(HTTP_STATUS.INTERNAL_ERROR); expect(proxy.failed_requests).toBe(2); expect(err.message).toMatch(API_ERROR.UPLINK_OFFLINE); done(); }); }); }); }); }, 10000); }); describe('isUplinkValid', () => { describe('valid use cases', () => { const validateUpLink = ( url: string, tarBallUrl = `${url}/artifactory/api/npm/npm/pk1-juan/-/pk1-juan-1.0.7.tgz` ) => { const uplinkConf = { url }; const proxy: any = generateProxy(uplinkConf); return proxy.isUplinkValid(tarBallUrl); }; test('should validate tarball path against uplink', () => { expect(validateUpLink('https://artifactory.mydomain.com')).toBe(true); }); test('should validate tarball path against uplink case#2', () => { expect(validateUpLink('https://artifactory.mydomain.com:443')).toBe(true); }); test('should validate tarball path against uplink case#3', () => { expect(validateUpLink('http://localhost')).toBe(true); }); test('should validate tarball path against uplink case#4', () => { expect(validateUpLink('http://my.domain.test')).toBe(true); }); test('should validate tarball path against uplink case#5', () => { expect(validateUpLink('http://my.domain.test:3000')).toBe(true); }); // corner case https://github.com/verdaccio/verdaccio/issues/571 test('should validate tarball path against uplink case#6', () => { // same protocol, same domain, port === 443 which is also the standard for https expect( validateUpLink( 'https://my.domain.test', `https://my.domain.test:443/artifactory/api/npm/npm/pk1-juan/-/pk1-juan-1.0.7.tgz` ) ).toBe(true); }); test('should validate tarball path against uplink case#7', () => { expect(validateUpLink('https://artifactory.mydomain.com:5569')).toBe(true); }); test('should validate tarball path against uplink case#8', () => { expect(validateUpLink('https://localhost:5539')).toBe(true); }); }); describe('invalid use cases', () => { test('should fails on validate tarball path against uplink', () => { const url = 'https://artifactory.mydomain.com'; const tarBallUrl = 'https://localhost/api/npm/npm/pk1-juan/-/pk1-juan-1.0.7.tgz'; const uplinkConf = { url }; const proxy: any = generateProxy(uplinkConf); expect(proxy.isUplinkValid(tarBallUrl)).toBe(false); }); test('should fails on validate tarball path against uplink case#2', () => { // different domain same, same port, same protocol const url = 'https://domain'; const tarBallUrl = 'https://localhost/api/npm/npm/pk1-juan/-/pk1-juan-1.0.7.tgz'; const uplinkConf = { url }; const proxy: any = generateProxy(uplinkConf); expect(proxy.isUplinkValid(tarBallUrl)).toBe(false); }); test('should fails on validate tarball path against uplink case#3', () => { // same domain, different protocol, different port const url = 'http://localhost:5001'; const tarBallUrl = 'https://localhost:4000/api/npm/npm/pk1-juan/-/pk1-juan-1.0.7.tgz'; const uplinkConf = { url }; const proxy: any = generateProxy(uplinkConf); expect(proxy.isUplinkValid(tarBallUrl)).toBe(false); }); test('should fails on validate tarball path against uplink case#4', () => { // same domain, same protocol, different port const url = 'https://subdomain.domain:5001'; const tarBallUrl = 'https://subdomain.domain:4000/api/npm/npm/pk1-juan/-/pk1-juan-1.0.7.tgz'; const uplinkConf = { url }; const proxy: any = generateProxy(uplinkConf); expect(proxy.isUplinkValid(tarBallUrl)).toBe(false); }); test('should fails on validate tarball path against uplink case#5', () => { // different protocol, different domain, different port const url = 'https://subdomain.my:5001'; const tarBallUrl = 'http://subdomain.domain:4000/api/npm/npm/pk1-juan/-/pk1-juan-1.0.7.tgz'; const uplinkConf = { url }; const proxy: any = generateProxy(uplinkConf); expect(proxy.isUplinkValid(tarBallUrl)).toBe(false); }); }); }); });