diff --git a/src/api/endpoint/api/package.ts b/src/api/endpoint/api/package.ts index 9bf60695d..f76781c38 100644 --- a/src/api/endpoint/api/package.ts +++ b/src/api/endpoint/api/package.ts @@ -34,32 +34,39 @@ const downloadStream = ( stream.pipe(res); }; -const redirectOrDownloadStream = ( +const redirectOrDownloadStream = async ( packageName: string, filename: string, storage: any, req: $RequestExtend, res: $ResponseExtend, config: Config -): void => { - const tarballUrlRedirect = _.get(config, 'experiments.tarball_url_redirect'); - storage - .hasLocalTarball(packageName, filename) - .then((hasLocalTarball) => { - if (hasLocalTarball) { - const context = { packageName, filename }; - const tarballUrl = - typeof tarballUrlRedirect === 'function' - ? tarballUrlRedirect(context) - : _.template(tarballUrlRedirect)(context); - res.redirect(tarballUrl); +): Promise => { + try { + const hasLocalTarball = await storage.hasLocalTarball(packageName, filename); + + if (!hasLocalTarball) { + return downloadStream(packageName, filename, storage, req, res); + } + + const tarballUrlRedirect = _.get(config, 'experiments.tarball_url_redirect'); + const context = { packageName, filename }; + let tarballUrl; + + if (typeof tarballUrlRedirect === 'function') { + if (tarballUrlRedirect.constructor.name === 'AsyncFunction') { + tarballUrl = await tarballUrlRedirect(context); } else { - downloadStream(packageName, filename, storage, req, res); + tarballUrl = tarballUrlRedirect(context); } - }) - .catch((err) => { - res.locals.report_error(err); - }); + } else { + tarballUrl = _.template(tarballUrlRedirect)(context); + } + + res.redirect(tarballUrl); + } catch (err) { + res.locals.report_error(err); + } }; export default function (route: Router, auth: Auth, storage: Storage, config: Config): void { diff --git a/test/helpers/initializeServer.ts b/test/helpers/initializeServer.ts index 71a1ea6cf..685bc7165 100644 --- a/test/helpers/initializeServer.ts +++ b/test/helpers/initializeServer.ts @@ -5,6 +5,7 @@ import path from 'path'; import { errorUtils } from '@verdaccio/core'; import { final } from '@verdaccio/middleware'; +import { ConfigYaml } from '@verdaccio/types'; import { generateRandomHexString } from '@verdaccio/utils'; import { errorReportingMiddleware, handleError } from '../../src/api/middleware'; @@ -14,12 +15,12 @@ import Config from '../../src/lib/config'; const debug = buildDebug('verdaccio:tools:helpers:server'); export async function initializeServer( - configName, + configuration: ConfigYaml, routesMiddleware: any[] = [], Storage ): Promise { const app = express(); - const config = new Config(configName); + const config = new Config(configuration); config.storage = path.join(os.tmpdir(), '/storage', generateRandomHexString()); // httpass would get path.basename() for configPath thus we need to create a dummy folder // to avoid conflics diff --git a/test/unit/modules/api/_helper.ts b/test/unit/modules/api/_helper.ts index acefc2b4b..d560dadb0 100644 --- a/test/unit/modules/api/_helper.ts +++ b/test/unit/modules/api/_helper.ts @@ -5,7 +5,7 @@ import supertest from 'supertest'; import { parseConfigFile } from '@verdaccio/config'; import { HEADERS, HEADER_TYPE, HTTP_STATUS, TOKEN_BEARER } from '@verdaccio/core'; -import { GenericBody, PackageUsers } from '@verdaccio/types'; +import { ConfigYaml, GenericBody, PackageUsers } from '@verdaccio/types'; import { buildToken, generateRandomHexString } from '@verdaccio/utils'; import apiMiddleware from '../../../../src/api/endpoint'; @@ -24,11 +24,15 @@ export const getConf = (conf) => { return config; }; -export async function initializeServer(configName): Promise { +export async function initializeServer(configName: string): Promise { const config = getConf(configName); return initializeServerHelper(config, [apiMiddleware], Storage); } +export async function initializeServerWithConfig(config: ConfigYaml): Promise { + return initializeServerHelper(config, [apiMiddleware], Storage); +} + export function createUser(app, name: string, password: string): supertest.Test { return supertest(app) .put(`/-/user/org.couchdb.user:${name}`) diff --git a/test/unit/modules/api/config/experiments.yaml b/test/unit/modules/api/config/experiments.yaml new file mode 100644 index 000000000..50fd5324d --- /dev/null +++ b/test/unit/modules/api/config/experiments.yaml @@ -0,0 +1,25 @@ +storage: ./storage_experiments + +auth: + htpasswd: + file: ./htpasswd-package + +web: + enable: true + title: verdaccio + +publish: + allow_offline: false + +uplinks: + +log: { type: stdout, format: pretty, level: trace } + +packages: + '@*/*': + access: $anonymous + publish: $anonymous + '**': + access: $anonymous + publish: $anonymous +_debug: true diff --git a/test/unit/modules/api/experiments.spec.ts b/test/unit/modules/api/experiments.spec.ts new file mode 100644 index 000000000..214733344 --- /dev/null +++ b/test/unit/modules/api/experiments.spec.ts @@ -0,0 +1,150 @@ +import supertest from 'supertest'; + +import { DIST_TAGS, HEADERS, HEADER_TYPE, HTTP_STATUS } from '@verdaccio/core'; + +import { getConf, initializeServer, initializeServerWithConfig, publishVersion } from './_helper'; + +describe('experiments', () => { + describe('for a function value of tarball_url_redirect', () => { + let app; + beforeEach(async () => { + const baseTestConfig = getConf('experiments.yaml'); + app = await initializeServerWithConfig({ + ...baseTestConfig, + experiments: { + // @ts-ignore + tarball_url_redirect(context) { + return `https://myapp.sfo1.mycdn.com/verdaccio/${context.packageName}/${context.filename}`; + }, + }, + }); + await publishVersion(app, '@tarball_tester/testTarballPackage', '1.0.0'); + await publishVersion(app, 'testTarballPackage', '1.0.0'); + }); + + test('should redirect for package tarball as function', (done) => { + supertest(app) + .get('/testTarballPackage/-/testTarballPackage-1.0.0.tgz') + .expect(302) + .end(function (err, res) { + if (err) { + return done(err); + } + expect(res.headers.location).toEqual( + 'https://myapp.sfo1.mycdn.com/verdaccio/testTarballPackage/testTarballPackage-1.0.0.tgz' + ); + done(); + }); + }); + + test('should redirect for scoped package tarball', (done) => { + supertest(app) + .get('/@tarball_tester/testTarballPackage/-/testTarballPackage-1.0.0.tgz') + .expect(302) + .end(function (err, res) { + if (err) { + return done(err); + } + expect(res.headers.location).toEqual( + 'https://myapp.sfo1.mycdn.com/verdaccio/@tarball_tester/testTarballPackage/testTarballPackage-1.0.0.tgz' + ); + done(); + }); + }); + }); + describe('for a async function value of tarball_url_redirect', () => { + let app; + beforeEach(async () => { + const baseTestConfig = getConf('experiments.yaml'); + app = await initializeServerWithConfig({ + ...baseTestConfig, + experiments: { + // @ts-ignore + async tarball_url_redirect(context) { + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + return `https://myapp.sfo1.mycdn.com/verdaccio/${context.packageName}/${context.filename}`; + }, + }, + }); + await publishVersion(app, '@tarball_tester/testTarballPackage', '1.0.0'); + await publishVersion(app, 'testTarballPackage', '1.0.0'); + }); + + test('should redirect for package tarball', (done) => { + supertest(app) + .get('/testTarballPackage/-/testTarballPackage-1.0.0.tgz') + .expect(302) + .end(function (err, res) { + if (err) { + return done(err); + } + expect(res.headers.location).toEqual( + 'https://myapp.sfo1.mycdn.com/verdaccio/testTarballPackage/testTarballPackage-1.0.0.tgz' + ); + done(); + }); + }); + + test('should redirect for scoped package tarball', (done) => { + supertest(app) + .get('/@tarball_tester/testTarballPackage/-/testTarballPackage-1.0.0.tgz') + .expect(302) + .end(function (err, res) { + if (err) { + return done(err); + } + expect(res.headers.location).toEqual( + 'https://myapp.sfo1.mycdn.com/verdaccio/@tarball_tester/testTarballPackage/testTarballPackage-1.0.0.tgz' + ); + done(); + }); + }); + }); + describe('for a string value of tarball_url_redirect', () => { + let app; + beforeEach(async () => { + const baseTestConfig = getConf('experiments.yaml'); + app = await initializeServerWithConfig({ + ...baseTestConfig, + experiments: { + // @ts-ignore + tarball_url_redirect: 'https://myapp.sfo1.mycdn.com/verdaccio/${packageName}/${filename}', + }, + }); + await publishVersion(app, '@tarball_tester/testTarballPackage', '1.0.0'); + await publishVersion(app, 'testTarballPackage', '1.0.0'); + }); + + test('should redirect for scoped package tarball', (done) => { + supertest(app) + .get('/@tarball_tester/testTarballPackage/-/testTarballPackage-1.0.0.tgz') + .expect(302) + .end(function (err, res) { + if (err) { + return done(err); + } + expect(res.headers.location).toEqual( + 'https://myapp.sfo1.mycdn.com/verdaccio/@tarball_tester/testTarballPackage/testTarballPackage-1.0.0.tgz' + ); + done(); + }); + }); + + test('should redirect for package tarball', (done) => { + supertest(app) + .get('/testTarballPackage/-/testTarballPackage-1.0.0.tgz') + .expect(302) + .end(function (err, res) { + if (err) { + return done(err); + } + expect(res.headers.location).toEqual( + 'https://myapp.sfo1.mycdn.com/verdaccio/testTarballPackage/testTarballPackage-1.0.0.tgz' + ); + done(); + }); + }); + }); +}); diff --git a/test/unit/modules/api/legacy/api.__c.ts b/test/unit/modules/api/legacy/api.__c.ts index 7debc46c7..29a0c9479 100644 --- a/test/unit/modules/api/legacy/api.__c.ts +++ b/test/unit/modules/api/legacy/api.__c.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import nock from 'nock'; import path from 'path'; -import rimraf from 'rimraf'; +import { rimrafSync } from 'rimraf'; import { Readable } from 'stream'; import request from 'supertest'; @@ -63,44 +63,43 @@ describe('endpoint unit test', () => { const mockServerPort = 55549; let mockRegistry; - beforeAll(function (done) { + beforeAll(async function () { const store = path.join(__dirname, '../../partials/store/test-storage-api-spec'); - rimraf(store, async () => { - const configForTest = configDefault( - { - auth: { - htpasswd: { - file: './test-storage-api-spec/.htpasswd', - }, - }, - filters: { - '../../modules/api/partials/plugin/filter': { - pkg: 'npm_test', - version: '2.0.0', - }, - }, - storage: store, - self_path: store, - uplinks: { - npmjs: { - url: `http://${DOMAIN_SERVERS}:${mockServerPort}`, - }, - socketTimeout: { - url: `http://some.registry.timeout.com`, - max_fails: 2, - timeout: '1s', - fail_timeout: '1s', - }, - }, - log: { type: 'stdout', format: 'pretty', level: 'warn' }, - }, - 'api.spec.yaml' - ); + rimrafSync(store); - app = await endPointAPI(configForTest); - mockRegistry = await mockServer(mockServerPort).init(); - done(); - }); + const configForTest = configDefault( + { + auth: { + htpasswd: { + file: './test-storage-api-spec/.htpasswd', + }, + }, + filters: { + '../../modules/api/partials/plugin/filter': { + pkg: 'npm_test', + version: '2.0.0', + }, + }, + storage: store, + self_path: store, + uplinks: { + npmjs: { + url: `http://${DOMAIN_SERVERS}:${mockServerPort}`, + }, + socketTimeout: { + url: `http://some.registry.timeout.com`, + max_fails: 2, + timeout: '1s', + fail_timeout: '1s', + }, + }, + log: { type: 'stdout', format: 'pretty', level: 'warn' }, + }, + 'api.spec.yaml' + ); + + app = await endPointAPI(configForTest); + mockRegistry = await mockServer(mockServerPort).init(); }); afterAll(function (done) { @@ -1016,136 +1015,6 @@ describe('endpoint unit test', () => { }); }); - describe('should test tarball url redirect', () => { - const pkgName = 'testTarballPackage'; - const scopedPkgName = '@tarball_tester/testTarballPackage'; - const tarballUrlRedirectCredentials = { name: 'tarball_tester', password: 'secretPass' }; - const store = path.join(__dirname, '../../partials/store/test-storage-api-spec'); - const mockServerPort = 55549; - const baseTestConfig = configDefault( - { - auth: { - htpasswd: { - file: './test-storage-api-spec/.htpasswd', - }, - }, - filters: { - '../../modules/api/partials/plugin/filter': { - pkg: 'npm_test', - version: '2.0.0', - }, - }, - storage: store, - self_path: store, - uplinks: { - npmjs: { - url: `http://${DOMAIN_SERVERS}:${mockServerPort}`, - }, - }, - log: { type: 'stdout', format: 'pretty', level: 'warn' }, - }, - 'api.spec.yaml' - ); - let token; - beforeAll(async () => { - token = await getNewToken(request(app), tarballUrlRedirectCredentials); - await putPackage(request(app), `/${pkgName}`, generatePackageMetadata(pkgName), token); - await putPackage( - request(app), - `/${scopedPkgName}`, - generatePackageMetadata(scopedPkgName), - token - ); - }); - - describe('for a string value of tarball_url_redirect', () => { - let app2; - beforeAll(async () => { - app2 = await endPointAPI({ - ...baseTestConfig, - experiments: { - tarball_url_redirect: - 'https://myapp.sfo1.mycdn.com/verdaccio/${packageName}/${filename}', - }, - }); - }); - - test('should redirect for package tarball', (done) => { - request(app2) - .get('/testTarballPackage/-/testTarballPackage-1.0.0.tgz') - .expect(HTTP_STATUS.REDIRECT) - .end(function (err, res) { - if (err) { - return done(err); - } - expect(res.headers.location).toEqual( - 'https://myapp.sfo1.mycdn.com/verdaccio/testTarballPackage/testTarballPackage-1.0.0.tgz' - ); - done(); - }); - }); - - test('should redirect for scoped package tarball', (done) => { - request(app2) - .get('/@tarball_tester/testTarballPackage/-/testTarballPackage-1.0.0.tgz') - .expect(HTTP_STATUS.REDIRECT) - .end(function (err, res) { - if (err) { - return done(err); - } - expect(res.headers.location).toEqual( - 'https://myapp.sfo1.mycdn.com/verdaccio/@tarball_tester/testTarballPackage/testTarballPackage-1.0.0.tgz' - ); - done(); - }); - }); - }); - - describe('for a function value of tarball_url_redirect', () => { - let app2; - beforeAll(async () => { - app2 = await endPointAPI({ - ...baseTestConfig, - experiments: { - tarball_url_redirect(context) { - return `https://myapp.sfo1.mycdn.com/verdaccio/${context.packageName}/${context.filename}`; - }, - }, - }); - }); - - test('should redirect for package tarball', (done) => { - request(app2) - .get('/testTarballPackage/-/testTarballPackage-1.0.0.tgz') - .expect(HTTP_STATUS.REDIRECT) - .end(function (err, res) { - if (err) { - return done(err); - } - expect(res.headers.location).toEqual( - 'https://myapp.sfo1.mycdn.com/verdaccio/testTarballPackage/testTarballPackage-1.0.0.tgz' - ); - done(); - }); - }); - - test('should redirect for scoped package tarball', (done) => { - request(app2) - .get('/@tarball_tester/testTarballPackage/-/testTarballPackage-1.0.0.tgz') - .expect(HTTP_STATUS.REDIRECT) - .end(function (err, res) { - if (err) { - return done(err); - } - expect(res.headers.location).toEqual( - 'https://myapp.sfo1.mycdn.com/verdaccio/@tarball_tester/testTarballPackage/testTarballPackage-1.0.0.tgz' - ); - done(); - }); - }); - }); - }); - describe('should test (un)deprecate api', () => { const pkgName = '@scope/deprecate'; const credentials = { name: 'jota_deprecate', password: 'secretPass' };