From ce013d2fcc2a8a3ab89ecd5cdabc6d54089d0581 Mon Sep 17 00:00:00 2001 From: Juan Picado Date: Sat, 1 Oct 2022 00:14:20 +0200 Subject: [PATCH] refactor: reimplement star command (#3410) --- .changeset/rich-badgers-begin.md | 9 + e2e/cli/README.md | 1 + e2e/cli/e2e-npm6/star.spec.ts | 69 +++++ e2e/cli/e2e-npm7/star.spec.ts | 69 +++++ e2e/cli/e2e-npm8/star.spec.ts | 69 +++++ e2e/cli/e2e-npm9/star.spec.ts | 69 +++++ e2e/cli/e2e-pnpm6/star.spec.ts | 69 +++++ e2e/cli/e2e-pnpm7/star.spec.ts | 69 +++++ packages/api/src/publish.ts | 16 +- packages/api/test/integration/publish.spec.ts | 4 +- packages/core/url/src/index.ts | 13 + packages/store/jest.config.js | 6 +- packages/store/src/lib/star-utils.ts | 53 ++-- packages/store/src/storage.ts | 88 +++++-- packages/store/src/type.ts | 17 +- packages/store/test/star-utils.test.ts | 58 +++++ packages/store/test/star.spec.ts | 31 --- packages/store/test/storage.spec.ts | 213 +++++++++++++++- packages/tools/helpers/src/addNewVersion.ts | 60 +++++ .../src/generateLocalPackageMetadata.ts | 67 +++++ .../helpers/src/generatePackageMetadata.ts | 235 +----------------- .../src/generateRemotePackageMetadata.ts | 86 +++++++ .../src/getDeprecatedPackageMetadata.ts | 19 ++ packages/tools/helpers/src/index.ts | 12 +- packages/tools/helpers/src/types.ts | 3 + packages/tools/helpers/src/utils.ts | 9 + packages/tools/helpers/tests/metadata.spec.ts | 5 +- packages/verdaccio/src/server/request.ts | 8 +- packages/verdaccio/test/basic.spec.ts | 6 +- packages/verdaccio/test/massive.spec.ts | 7 +- 30 files changed, 1086 insertions(+), 354 deletions(-) create mode 100644 .changeset/rich-badgers-begin.md create mode 100644 e2e/cli/e2e-npm6/star.spec.ts create mode 100644 e2e/cli/e2e-npm7/star.spec.ts create mode 100644 e2e/cli/e2e-npm8/star.spec.ts create mode 100644 e2e/cli/e2e-npm9/star.spec.ts create mode 100644 e2e/cli/e2e-pnpm6/star.spec.ts create mode 100644 e2e/cli/e2e-pnpm7/star.spec.ts create mode 100644 packages/store/test/star-utils.test.ts delete mode 100644 packages/store/test/star.spec.ts create mode 100644 packages/tools/helpers/src/addNewVersion.ts create mode 100644 packages/tools/helpers/src/generateLocalPackageMetadata.ts create mode 100644 packages/tools/helpers/src/generateRemotePackageMetadata.ts create mode 100644 packages/tools/helpers/src/getDeprecatedPackageMetadata.ts create mode 100644 packages/tools/helpers/src/types.ts diff --git a/.changeset/rich-badgers-begin.md b/.changeset/rich-badgers-begin.md new file mode 100644 index 000000000..ddce3a191 --- /dev/null +++ b/.changeset/rich-badgers-begin.md @@ -0,0 +1,9 @@ +--- +'@verdaccio/api': minor +'@verdaccio/url': minor +'@verdaccio/store': minor +'@verdaccio/test-helper': minor +'verdaccio': minor +--- + +refactor: npm star command support reimplemented diff --git a/e2e/cli/README.md b/e2e/cli/README.md index 5a4bd140e..c6bad8009 100644 --- a/e2e/cli/README.md +++ b/e2e/cli/README.md @@ -19,6 +19,7 @@ | deprecate | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | | ping | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | | search | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | +| star | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ | | dist-tag | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | > notes: diff --git a/e2e/cli/e2e-npm6/star.spec.ts b/e2e/cli/e2e-npm6/star.spec.ts new file mode 100644 index 000000000..812206b57 --- /dev/null +++ b/e2e/cli/e2e-npm6/star.spec.ts @@ -0,0 +1,69 @@ +import { + addRegistry, + initialSetup, + npmUtils, + prepareGenericEmptyProject, +} from '@verdaccio/test-cli-commons'; + +import { npm } from './utils'; + +describe('star a package', () => { + jest.setTimeout(20000); + let registry; + + beforeAll(async () => { + const setup = await initialSetup(); + registry = setup.registry; + await registry.init(); + }); + + test.each([['@verdaccio/foo']])('should star a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await npmUtils.publish(npm, tempFolder, pkgName, registry); + const resp = await npm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + }); + + test.each([['@verdaccio/bar']])('should unstar a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await npmUtils.publish(npm, tempFolder, pkgName, registry); + const resp = await npm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + + const resp1 = await npm( + { cwd: tempFolder }, + 'unstar', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp1.stdout).toEqual(`☆ ${pkgName}`); + }); + + afterAll(async () => { + registry.stop(); + }); +}); diff --git a/e2e/cli/e2e-npm7/star.spec.ts b/e2e/cli/e2e-npm7/star.spec.ts new file mode 100644 index 000000000..812206b57 --- /dev/null +++ b/e2e/cli/e2e-npm7/star.spec.ts @@ -0,0 +1,69 @@ +import { + addRegistry, + initialSetup, + npmUtils, + prepareGenericEmptyProject, +} from '@verdaccio/test-cli-commons'; + +import { npm } from './utils'; + +describe('star a package', () => { + jest.setTimeout(20000); + let registry; + + beforeAll(async () => { + const setup = await initialSetup(); + registry = setup.registry; + await registry.init(); + }); + + test.each([['@verdaccio/foo']])('should star a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await npmUtils.publish(npm, tempFolder, pkgName, registry); + const resp = await npm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + }); + + test.each([['@verdaccio/bar']])('should unstar a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await npmUtils.publish(npm, tempFolder, pkgName, registry); + const resp = await npm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + + const resp1 = await npm( + { cwd: tempFolder }, + 'unstar', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp1.stdout).toEqual(`☆ ${pkgName}`); + }); + + afterAll(async () => { + registry.stop(); + }); +}); diff --git a/e2e/cli/e2e-npm8/star.spec.ts b/e2e/cli/e2e-npm8/star.spec.ts new file mode 100644 index 000000000..812206b57 --- /dev/null +++ b/e2e/cli/e2e-npm8/star.spec.ts @@ -0,0 +1,69 @@ +import { + addRegistry, + initialSetup, + npmUtils, + prepareGenericEmptyProject, +} from '@verdaccio/test-cli-commons'; + +import { npm } from './utils'; + +describe('star a package', () => { + jest.setTimeout(20000); + let registry; + + beforeAll(async () => { + const setup = await initialSetup(); + registry = setup.registry; + await registry.init(); + }); + + test.each([['@verdaccio/foo']])('should star a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await npmUtils.publish(npm, tempFolder, pkgName, registry); + const resp = await npm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + }); + + test.each([['@verdaccio/bar']])('should unstar a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await npmUtils.publish(npm, tempFolder, pkgName, registry); + const resp = await npm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + + const resp1 = await npm( + { cwd: tempFolder }, + 'unstar', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp1.stdout).toEqual(`☆ ${pkgName}`); + }); + + afterAll(async () => { + registry.stop(); + }); +}); diff --git a/e2e/cli/e2e-npm9/star.spec.ts b/e2e/cli/e2e-npm9/star.spec.ts new file mode 100644 index 000000000..812206b57 --- /dev/null +++ b/e2e/cli/e2e-npm9/star.spec.ts @@ -0,0 +1,69 @@ +import { + addRegistry, + initialSetup, + npmUtils, + prepareGenericEmptyProject, +} from '@verdaccio/test-cli-commons'; + +import { npm } from './utils'; + +describe('star a package', () => { + jest.setTimeout(20000); + let registry; + + beforeAll(async () => { + const setup = await initialSetup(); + registry = setup.registry; + await registry.init(); + }); + + test.each([['@verdaccio/foo']])('should star a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await npmUtils.publish(npm, tempFolder, pkgName, registry); + const resp = await npm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + }); + + test.each([['@verdaccio/bar']])('should unstar a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await npmUtils.publish(npm, tempFolder, pkgName, registry); + const resp = await npm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + + const resp1 = await npm( + { cwd: tempFolder }, + 'unstar', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp1.stdout).toEqual(`☆ ${pkgName}`); + }); + + afterAll(async () => { + registry.stop(); + }); +}); diff --git a/e2e/cli/e2e-pnpm6/star.spec.ts b/e2e/cli/e2e-pnpm6/star.spec.ts new file mode 100644 index 000000000..7ac3e29ff --- /dev/null +++ b/e2e/cli/e2e-pnpm6/star.spec.ts @@ -0,0 +1,69 @@ +import { + addRegistry, + initialSetup, + pnpmUtils, + prepareGenericEmptyProject, +} from '@verdaccio/test-cli-commons'; + +import { pnpm } from './utils'; + +describe('star a package', () => { + jest.setTimeout(20000); + let registry; + + beforeAll(async () => { + const setup = await initialSetup(); + registry = setup.registry; + await registry.init(); + }); + + test.each([['@verdaccio/foo']])('should star a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await pnpmUtils.publish(pnpm, tempFolder, pkgName, registry); + const resp = await pnpm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + }); + + test.each([['@verdaccio/bar']])('should unstar a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await pnpmUtils.publish(pnpm, tempFolder, pkgName, registry); + const resp = await pnpm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + + const resp1 = await pnpm( + { cwd: tempFolder }, + 'unstar', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp1.stdout).toEqual(`☆ ${pkgName}`); + }); + + afterAll(async () => { + registry.stop(); + }); +}); diff --git a/e2e/cli/e2e-pnpm7/star.spec.ts b/e2e/cli/e2e-pnpm7/star.spec.ts new file mode 100644 index 000000000..7ac3e29ff --- /dev/null +++ b/e2e/cli/e2e-pnpm7/star.spec.ts @@ -0,0 +1,69 @@ +import { + addRegistry, + initialSetup, + pnpmUtils, + prepareGenericEmptyProject, +} from '@verdaccio/test-cli-commons'; + +import { pnpm } from './utils'; + +describe('star a package', () => { + jest.setTimeout(20000); + let registry; + + beforeAll(async () => { + const setup = await initialSetup(); + registry = setup.registry; + await registry.init(); + }); + + test.each([['@verdaccio/foo']])('should star a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await pnpmUtils.publish(pnpm, tempFolder, pkgName, registry); + const resp = await pnpm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + }); + + test.each([['@verdaccio/bar']])('should unstar a package %s', async (pkgName) => { + const { tempFolder } = await prepareGenericEmptyProject( + pkgName, + '1.0.0-patch', + registry.port, + registry.getToken(), + registry.getRegistryUrl() + ); + + await pnpmUtils.publish(pnpm, tempFolder, pkgName, registry); + const resp = await pnpm( + { cwd: tempFolder }, + 'star', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp.stdout).toEqual(`★ ${pkgName}`); + + const resp1 = await pnpm( + { cwd: tempFolder }, + 'unstar', + pkgName, + ...addRegistry(registry.getRegistryUrl()) + ); + expect(resp1.stdout).toEqual(`☆ ${pkgName}`); + }); + + afterAll(async () => { + registry.stop(); + }); +}); diff --git a/packages/api/src/publish.ts b/packages/api/src/publish.ts index dc0ab089a..fcffa5afd 100644 --- a/packages/api/src/publish.ts +++ b/packages/api/src/publish.ts @@ -11,7 +11,6 @@ import { Storage } from '@verdaccio/store'; import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom'; // import star from './star'; -// import { isPublishablePackage, isRelatedToDeprecation } from './utils'; const debug = buildDebug('verdaccio:api:publish'); @@ -177,17 +176,17 @@ export default function publish(router: Router, auth: IAuth, storage: Storage): export function publishPackage(storage: Storage): any { return async function ( req: $RequestExtend, - _res: $ResponseExtend, + res: $ResponseExtend, next: $NextFunctionVer ): Promise { const ac = new AbortController(); const packageName = req.params.package; const { revision } = req.params; const metadata = req.body; + const username = req?.remote_user?.name; try { - debug('publishing %s', packageName); - await storage.updateManifest(metadata, { + const message = await storage.updateManifest(metadata, { name: packageName, revision, signal: ac.signal, @@ -196,16 +195,15 @@ export function publishPackage(storage: Storage): any { protocol: req.protocol, // @ts-ignore headers: req.headers, + username, }, }); - _res.status(HTTP_STATUS.CREATED); + + res.status(HTTP_STATUS.CREATED); return next({ - // TODO: this could be also Package Updated based on the - // action, deprecate, star, publish new version, or create a package - // the message some return from the method - ok: API_MESSAGE.PKG_CREATED, success: true, + ok: message, }); } catch (err: any) { // TODO: review if we need the abort controller here diff --git a/packages/api/test/integration/publish.spec.ts b/packages/api/test/integration/publish.spec.ts index daa91d2ed..207f344f8 100644 --- a/packages/api/test/integration/publish.spec.ts +++ b/packages/api/test/integration/publish.spec.ts @@ -160,13 +160,13 @@ describe('publish', () => { decodeURIComponent(pkgName), '1.0.1-patch' ).expect(HTTP_STATUS.CREATED); - expect(response.body.ok).toEqual(API_MESSAGE.PKG_CREATED); + expect(response.body.ok).toEqual(API_MESSAGE.PKG_CHANGED); const response2 = await publishVersion( app, decodeURIComponent(pkgName), '1.0.2-patch' ).expect(HTTP_STATUS.CREATED); - expect(response2.body.ok).toEqual(API_MESSAGE.PKG_CREATED); + expect(response2.body.ok).toEqual(API_MESSAGE.PKG_CHANGED); } ); }); diff --git a/packages/core/url/src/index.ts b/packages/core/url/src/index.ts index b5f942935..5d7af48ce 100644 --- a/packages/core/url/src/index.ts +++ b/packages/core/url/src/index.ts @@ -90,10 +90,23 @@ export function validateURL(publicUrl: string | void) { } export type RequestOptions = { + /** + * Request host. + */ host: string; + /** + * Request protocol. + */ protocol: string; + /** + * Request headers. + */ headers: { [key: string]: string }; remoteAddress?: string; + /** + * Logged username the request, usually after token verification. + */ + username?: string; }; export function getPublicUrl(url_prefix: string = '', requestOptions: RequestOptions): string { diff --git a/packages/store/jest.config.js b/packages/store/jest.config.js index b4b5ec7c9..6c12a1a5c 100644 --- a/packages/store/jest.config.js +++ b/packages/store/jest.config.js @@ -4,9 +4,9 @@ module.exports = Object.assign({}, config, { coverageThreshold: { global: { // FIXME: increase to 90 - branches: 51, - functions: 69, - lines: 66, + branches: 62, + functions: 86, + lines: 76, }, }, }); diff --git a/packages/store/src/lib/star-utils.ts b/packages/store/src/lib/star-utils.ts index e44f4a667..1399106c0 100644 --- a/packages/store/src/lib/star-utils.ts +++ b/packages/store/src/lib/star-utils.ts @@ -1,17 +1,8 @@ -import _ from 'lodash'; - import { validatioUtils } from '@verdaccio/core'; -import { Manifest } from '@verdaccio/types'; - -import { Users } from '../type'; +import { Manifest, PackageUsers } from '@verdaccio/types'; /** - * Check whether the package metadta has enough data to be published - * @param pkg metadata - */ - -/** - * Check whether the package metadta has enough data to be published + * Check whether the package metadata has enough data to be published * @param pkg metadata */ export function isPublishablePackage(pkg: Manifest): boolean { @@ -21,27 +12,31 @@ export function isPublishablePackage(pkg: Manifest): boolean { return keys.includes('versions'); } -// @deprecated don't think this is used anymore (REMOVE) -export function isRelatedToDeprecation(pkgInfo: Manifest): boolean { - const { versions } = pkgInfo; - for (const version in versions) { - if (Object.prototype.hasOwnProperty.call(versions[version], 'deprecated')) { - return true; - } - } - return false; -} - -export function validateInputs(localUsers: Users, username: string, isStar: boolean): boolean { - const isExistlocalUsers = _.isNil(localUsers[username]) === false; - if (isStar && isExistlocalUsers && localUsers[username]) { - return true; - } else if (!isStar && isExistlocalUsers) { +/** + * Verify if the user is actually executing an action, to avoid unnecessary calls + * to the storage. + * @param localUsers current state at cache + * @param username user is executing the action + * @param userIsAddingStar whether user is removing or adding star + * @returns boolean + */ +export function isExecutingStarCommand( + localUsers: PackageUsers, + username: string, + userIsAddingStar: boolean +): boolean { + const isExist = typeof localUsers[username] !== 'undefined'; + // fails if user already exist and us trying to add star. + if (userIsAddingStar && isExist && localUsers[username]) { return false; - } else if (!isStar && !isExistlocalUsers) { + // if is not adding a start but user exists (removing star) + } else if (!userIsAddingStar && isExist) { return true; + // fails if user does not exist and is not adding any star + } else if (!userIsAddingStar && !isExist) { + return false; } - return false; + return true; } export function isStarManifest(manifest: Manifest): boolean { diff --git a/packages/store/src/storage.ts b/packages/store/src/storage.ts index ef8ff30c3..f764f2e7f 100644 --- a/packages/store/src/storage.ts +++ b/packages/store/src/storage.ts @@ -9,6 +9,7 @@ import { default as URL } from 'url'; import { hasProxyTo } from '@verdaccio/config'; import { API_ERROR, + API_MESSAGE, DIST_TAGS, HEADER_TYPE, HTTP_STATUS, @@ -38,6 +39,7 @@ import { Logger, Manifest, MergeTags, + PackageUsers, StringValue, Token, TokenFilter, @@ -57,6 +59,7 @@ import { import { TransFormResults } from './lib/TransFormResults'; import { removeDuplicates } from './lib/search-utils'; import { isPublishablePackage } from './lib/star-utils'; +import { isExecutingStarCommand } from './lib/star-utils'; import { STORAGE, cleanUpLinksRef, @@ -72,7 +75,7 @@ import { import { ProxyInstanceList, setupUpLinks, updateVersionsHiddenUpLinkNext } from './lib/uplink-util'; import { getVersion } from './lib/versions-utils'; import { LocalStorage } from './local-storage'; -import { IGetPackageOptionsNext, IPluginFilters } from './type'; +import { IGetPackageOptionsNext, IPluginFilters, StarManifestBody } from './type'; const debug = buildDebug('verdaccio:storage'); @@ -915,26 +918,33 @@ class Storage { return uplink; } - public async updateManifest(manifest: Manifest, options: UpdateManifestOptions): Promise { - if (isDeprecatedManifest(manifest)) { + public async updateManifest( + manifest: Manifest | StarManifestBody, + options: UpdateManifestOptions + ): Promise { + if (isDeprecatedManifest(manifest as Manifest)) { // if the manifest is deprecated, we need to update the package.json - await this.deprecate(manifest, { + await this.deprecate(manifest as Manifest, { ...options, }); } else if ( - isPublishablePackage(manifest) === false && + isPublishablePackage(manifest as Manifest) === false && validatioUtils.isObject(manifest.users) ) { // if user request to apply a star to the manifest - await this.star(manifest, { + await this.star(manifest as StarManifestBody, { ...options, }); + return API_MESSAGE.PKG_CHANGED; } else if (validatioUtils.validatePublishSingleVersion(manifest)) { // if continue, the version to be published does not exist // we create a new package - const [mergedManifest, version] = await this.publishANewVersion(manifest, { - ...options, - }); + const [mergedManifest, version, message] = await this.publishANewVersion( + manifest as Manifest, + { + ...options, + } + ); // send notification of publication (notification step, non transactional) try { const { name } = mergedManifest; @@ -943,6 +953,7 @@ class Storage { } catch (error: any) { logger.error({ error: error.message }, 'notify batch service has failed: @{error}'); } + return message; } else { debug('invalid body format'); logger.info( @@ -959,15 +970,51 @@ class Storage { return this.changePackage(name, manifest, options.revision as string); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - private async star(_body: Manifest, _options: PublishOptions): Promise { - // // const storage: IPackageStorage = this.getPrivatePackageStorage(opname); + private async star(manifest: StarManifestBody, options: UpdateManifestOptions): Promise { + const { users } = manifest; + const { requestOptions, name } = options; + debug('star %s', name); + const { username } = requestOptions; + if (!username) { + throw errorUtils.getBadRequest('update users only allowed to logged users'); + } - // if (typeof storage === 'undefined') { - // throw errorUtils.getNotFound(); - // } + const localPackage = await this.getPackageManifest({ + name, + requestOptions, + uplinksLook: false, + }); + // backward compatible in case users are not in the storage. + const localStarUsers = localPackage.users || {}; + // if trying to add a star + const userIsAddingStar = Object.keys(users as PackageUsers).includes(username); + if (!isExecutingStarCommand(localPackage.users as PackageUsers, username, userIsAddingStar)) { + return API_MESSAGE.PKG_CHANGED; + } - throw errorUtils.getInternalError('no implementation ready for npm star'); + const newUsers = userIsAddingStar + ? { + ...localStarUsers, + [username]: true, + } + : _.reduce( + localStarUsers, + (users, value, key) => { + if (key !== username) { + users[key] = value; + } + return users; + }, + {} + ); + + await this.changePackage( + name, + { ...localPackage, users: newUsers }, + options.revision as string + ); + + return API_MESSAGE.PKG_CHANGED; } /** @@ -1025,10 +1072,10 @@ class Storage { private async publishANewVersion( body: Manifest, options: PublishOptions - ): Promise<[Manifest, string]> { + ): Promise<[Manifest, string, string]> { const { name } = options; debug('publishing a new package for %o', name); - + let successResponseMessage; const manifest: Manifest = { ...validatioUtils.normalizeMetadata(body, name) }; const { _attachments, versions } = manifest; @@ -1066,6 +1113,9 @@ class Storage { const hasPackageInStorage = await this.hasPackage(name); if (!hasPackageInStorage) { await this.createNewLocalCachePackage(name, versionToPublish); + successResponseMessage = API_MESSAGE.PKG_CREATED; + } else { + successResponseMessage = API_MESSAGE.PKG_CHANGED; } } catch (err: any) { debug('error on change or update a package with %o', err.message); @@ -1120,7 +1170,7 @@ class Storage { 'package @{name}@@{version} has been published' ); - return [mergedManifest, versionToPublish]; + return [mergedManifest, versionToPublish, successResponseMessage]; } // TODO: pending implementation diff --git a/packages/store/src/type.ts b/packages/store/src/type.ts index d062770b0..21a869dfc 100644 --- a/packages/store/src/type.ts +++ b/packages/store/src/type.ts @@ -1,5 +1,5 @@ import { FetchOptions } from '@verdaccio/proxy'; -import { Config, IPluginStorageFilter, RemoteUser } from '@verdaccio/types'; +import { Config, IPluginStorageFilter, Manifest, RemoteUser } from '@verdaccio/types'; import { RequestOptions } from '@verdaccio/url'; // @deprecated use IGetPackageOptionsNext @@ -57,13 +57,10 @@ export type UpdateManifestOptions = { signal: AbortSignal; }; -export type Users = { - [key: string]: string; -}; -export interface StarBody { - _id: string; - _rev: string; - users: Users; -} - export type IPluginFilters = IPluginStorageFilter[]; + +/** + * When the command `npm star` is executed, the body only contains the following + * values in the body. + */ +export type StarManifestBody = Pick; diff --git a/packages/store/test/star-utils.test.ts b/packages/store/test/star-utils.test.ts new file mode 100644 index 000000000..f0ff2d31e --- /dev/null +++ b/packages/store/test/star-utils.test.ts @@ -0,0 +1,58 @@ +import { Manifest } from '@verdaccio/types'; + +import { generatePackageMetadata } from '../../api/node_modules/@verdaccio/test-helper/build'; +import { isExecutingStarCommand } from '../src'; +import { isStarManifest } from '../src'; + +describe('Star Utils', () => { + describe('isExecutingStarCommand', () => { + describe('disallow states', () => { + test('should not allow add star with no existing users', () => { + expect(isExecutingStarCommand({}, 'foo', false)).toBeFalsy(); + }); + + test('should not allow add star with existing users', () => { + expect(isExecutingStarCommand({ bar: true }, 'foo', false)).toBeFalsy(); + }); + + test('should fails if user already exist and us trying to add star', () => { + expect(isExecutingStarCommand({ foo: true }, 'foo', true)).toBeFalsy(); + }); + }); + + describe('allow states', () => { + test('should allow add star with existing users', () => { + expect(isExecutingStarCommand({ foo: true }, 'foo', false)).toBeTruthy(); + }); + + test('should allow if is adding star and does not exist', () => { + expect(isExecutingStarCommand({ foo: true }, 'bar', true)).toBeTruthy(); + }); + }); + }); + + describe('isStarManifest', () => { + test('is not star manifest', () => { + const pkg = generatePackageMetadata('foo'); + expect(isStarManifest(pkg)).toBe(false); + }); + + test('is not star manifest empty users', () => { + const pkg = generatePackageMetadata('foo'); + pkg.users = {}; + expect(isStarManifest(pkg)).toBe(false); + }); + + test('is star manifest', () => { + const pkg = generatePackageMetadata('foo', '3.0.0') as Manifest; + // Staring a package usually is without versions and the user property within + // the manifest body + // @ts-expect-error + delete pkg.versions; + pkg.users = { + foo: true, + }; + expect(isStarManifest(pkg)).toBe(true); + }); + }); +}); diff --git a/packages/store/test/star.spec.ts b/packages/store/test/star.spec.ts deleted file mode 100644 index 8cee76808..000000000 --- a/packages/store/test/star.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Manifest } from '@verdaccio/types'; - -import { generatePackageMetadata } from '../../api/node_modules/@verdaccio/test-helper/build'; -import { isStarManifest } from '../src'; - -describe('Star Utils', () => { - describe('isStarManifest', () => { - test('is not star manifest', () => { - const pkg = generatePackageMetadata('foo'); - expect(isStarManifest(pkg)).toBe(false); - }); - - test('is not star manifest empty users', () => { - const pkg = generatePackageMetadata('foo'); - pkg.users = {}; - expect(isStarManifest(pkg)).toBe(false); - }); - - test('is star manifest', () => { - const pkg = generatePackageMetadata('foo', '3.0.0') as Manifest; - // Staring a package usually is without versions and the user property within - // the manifest body - // @ts-expect-error - delete pkg.versions; - pkg.users = { - foo: true, - }; - expect(isStarManifest(pkg)).toBe(true); - }); - }); -}); diff --git a/packages/store/test/storage.spec.ts b/packages/store/test/storage.spec.ts index d8e851c91..4b5aad0a6 100644 --- a/packages/store/test/storage.spec.ts +++ b/packages/store/test/storage.spec.ts @@ -7,7 +7,15 @@ import os from 'os'; import path from 'path'; import { Config, getDefaultConfig } from '@verdaccio/config'; -import { API_ERROR, DIST_TAGS, HEADERS, HEADER_TYPE, errorUtils, fileUtils } from '@verdaccio/core'; +import { + API_ERROR, + API_MESSAGE, + DIST_TAGS, + HEADERS, + HEADER_TYPE, + errorUtils, + fileUtils, +} from '@verdaccio/core'; import { setup } from '@verdaccio/logger'; import { addNewVersion, @@ -16,7 +24,7 @@ import { generateRemotePackageMetadata, getDeprecatedPackageMetadata, } from '@verdaccio/test-helper'; -import { AbbreviatedManifest, ConfigYaml, Manifest, Version } from '@verdaccio/types'; +import { AbbreviatedManifest, ConfigYaml, Manifest, PackageUsers, Version } from '@verdaccio/types'; import { Storage } from '../src'; import manifestFooRemoteNpmjs from './fixtures/manifests/foo-npmjs.json'; @@ -56,6 +64,31 @@ const defaultRequestOptions = { headers: {}, }; +const executeStarPackage = async ( + storage, + options: { + users: PackageUsers; + username: string; + name: string; + _rev: string; + _id?: string; + } +) => { + const { name, _rev, _id, users, username } = options; + const starManifest = { + _rev, + _id, + users, + }; + return storage.updateManifest(starManifest, { + signal: new AbortController().signal, + name, + uplinksLook: true, + revision: '1', + requestOptions: { ...defaultRequestOptions, username }, + }); +}; + describe('storage', () => { beforeEach(() => { nock.cleanAll(); @@ -400,6 +433,182 @@ describe('storage', () => { expect(manifest3._rev !== deprecatedManifest._rev).toBeTruthy(); }); }); + describe('star', () => { + test.each([['foo']])('star package %s', async (pkgName) => { + const config = getConfig('deprecate.yaml'); + const storage = new Storage(config); + await storage.init(config); + const bodyNewManifest = generatePackageMetadata(pkgName, '1.0.0'); + await storage.updateManifest(bodyNewManifest, { + signal: new AbortController().signal, + name: pkgName, + uplinksLook: true, + revision: '1', + requestOptions: defaultRequestOptions, + }); + const message = await executeStarPackage(storage, { + _rev: bodyNewManifest._rev, + _id: bodyNewManifest._id, + name: pkgName, + username: 'fooUser', + users: { fooUser: true }, + }); + expect(message).toEqual(API_MESSAGE.PKG_CHANGED); + const manifest1 = (await storage.getPackageByOptions({ + name: pkgName, + uplinksLook: true, + requestOptions: defaultRequestOptions, + })) as Manifest; + + expect(manifest1?.users).toEqual({ + fooUser: true, + }); + }); + + test.each([['foo']])('should add multiple users to package %s', async (pkgName) => { + const mockDate = '2018-01-14T11:17:40.712Z'; + MockDate.set(mockDate); + const config = getConfig('deprecate.yaml'); + const storage = new Storage(config); + await storage.init(config); + const bodyNewManifest = generatePackageMetadata(pkgName, '1.0.0'); + await storage.updateManifest(bodyNewManifest, { + signal: new AbortController().signal, + name: pkgName, + uplinksLook: true, + revision: '1', + requestOptions: defaultRequestOptions, + }); + const message = await executeStarPackage(storage, { + _rev: bodyNewManifest._rev, + _id: bodyNewManifest._id, + name: pkgName, + username: 'fooUser', + users: { fooUser: true }, + }); + expect(message).toEqual(API_MESSAGE.PKG_CHANGED); + + await executeStarPackage(storage, { + _rev: bodyNewManifest._rev, + _id: bodyNewManifest._id, + name: pkgName, + username: 'owner', + users: { owner: true }, + }); + const manifest1 = (await storage.getPackageByOptions({ + name: pkgName, + uplinksLook: true, + requestOptions: defaultRequestOptions, + })) as Manifest; + + expect(manifest1?.users).toEqual({ + fooUser: true, + owner: true, + }); + }); + + test.each([['foo']])('should ignore duplicate users to package %s', async (pkgName) => { + const mockDate = '2018-01-14T11:17:40.712Z'; + MockDate.set(mockDate); + const config = getConfig('deprecate.yaml'); + const storage = new Storage(config); + await storage.init(config); + const bodyNewManifest = generatePackageMetadata(pkgName, '1.0.0'); + await storage.updateManifest(bodyNewManifest, { + signal: new AbortController().signal, + name: pkgName, + uplinksLook: true, + revision: '1', + requestOptions: defaultRequestOptions, + }); + const message = await executeStarPackage(storage, { + _rev: bodyNewManifest._rev, + _id: bodyNewManifest._id, + name: pkgName, + username: 'fooUser', + users: { fooUser: true }, + }); + expect(message).toEqual(API_MESSAGE.PKG_CHANGED); + + await executeStarPackage(storage, { + _rev: bodyNewManifest._rev, + _id: bodyNewManifest._id, + name: pkgName, + username: 'fooUser', + users: { fooUser: true }, + }); + const manifest1 = (await storage.getPackageByOptions({ + name: pkgName, + uplinksLook: true, + requestOptions: defaultRequestOptions, + })) as Manifest; + + expect(manifest1?.users).toEqual({ + fooUser: true, + }); + }); + + test.each([['foo']])('should unstar a package %s', async (pkgName) => { + const config = getConfig('deprecate.yaml'); + const storage = new Storage(config); + await storage.init(config); + const bodyNewManifest = generatePackageMetadata(pkgName, '1.0.0'); + await storage.updateManifest(bodyNewManifest, { + signal: new AbortController().signal, + name: pkgName, + uplinksLook: true, + revision: '1', + requestOptions: defaultRequestOptions, + }); + const message = await executeStarPackage(storage, { + _rev: bodyNewManifest._rev, + _id: bodyNewManifest._id, + name: pkgName, + username: 'fooUser', + users: { fooUser: true }, + }); + expect(message).toEqual(API_MESSAGE.PKG_CHANGED); + + await executeStarPackage(storage, { + _rev: bodyNewManifest._rev, + _id: bodyNewManifest._id, + name: pkgName, + username: 'fooUser', + users: {}, + }); + const manifest1 = (await storage.getPackageByOptions({ + name: pkgName, + uplinksLook: true, + requestOptions: defaultRequestOptions, + })) as Manifest; + + expect(manifest1?.users).toEqual({}); + }); + + test.each([['foo']])('should handle missing username %s', async (pkgName) => { + const config = getConfig('deprecate.yaml'); + const storage = new Storage(config); + await storage.init(config); + const bodyNewManifest = generatePackageMetadata(pkgName, '1.0.0'); + await storage.updateManifest(bodyNewManifest, { + signal: new AbortController().signal, + name: pkgName, + uplinksLook: true, + revision: '1', + requestOptions: defaultRequestOptions, + }); + await expect( + executeStarPackage(storage, { + _rev: bodyNewManifest._rev, + _id: bodyNewManifest._id, + name: pkgName, + // @ts-expect-error + username: undefined, + users: { fooUser: true }, + }) + ).rejects.toThrow(); + }); + }); }); describe('getTarballNext', () => { diff --git a/packages/tools/helpers/src/addNewVersion.ts b/packages/tools/helpers/src/addNewVersion.ts new file mode 100644 index 000000000..4d6bd8c8b --- /dev/null +++ b/packages/tools/helpers/src/addNewVersion.ts @@ -0,0 +1,60 @@ +import { Manifest } from '@verdaccio/types'; + +import { getTarball } from './utils'; + +export function addNewVersion( + manifest: Manifest, + version: string, + isRemote: boolean = true, + domain: string = 'http://localhost:5555' +): Manifest { + const currentVersions = Object.keys(manifest.versions); + if (currentVersions.includes(version)) { + throw new Error(`Version ${version} already exists`); + } + + const newManifest = { ...manifest }; + newManifest.versions[version] = { + name: manifest.name, + version, + description: manifest.description ?? '', + readme: '', + main: 'index.js', + scripts: { test: 'echo "Error: no test specified" && exit 1' }, + keywords: [], + author: { name: 'User NPM', email: 'user@domain.com' }, + license: 'ISC', + dependencies: { verdaccio: '^2.7.2' }, + readmeFilename: 'README.md', + _id: `${manifest.name}@${version}`, + _npmVersion: '5.5.1', + _npmUser: { name: 'foo' }, + dist: { + integrity: 'sha512-6gHiERpiDgtb3hjqpQHoPoH4g==', + shasum: '2c03764f651a9f016ca0b7620421457b619151b9', + tarball: `${domain}/${manifest.name}/-/${getTarball(manifest.name)}-${version}.tgz`, + }, + contributors: [], + }; + // update the latest with the new version + newManifest['dist-tags'] = { latest: version }; + // add new version does not need attachments + if (isRemote) { + newManifest._distfiles = { + ...newManifest._distfiles, + [`${getTarball(manifest.name)}-${version}.tgz`]: { + sha: '2c03764f651a9f016ca0b7620421457b619151b9', + url: `${domain}/${manifest.name}/-/${getTarball(manifest.name)}-${version}.tgz`, + }, + }; + } else { + newManifest._attachments = { + ...newManifest._attachments, + [`${getTarball(manifest.name)}-${version}.tgz`]: { + shasum: '2c03764f651a9f016ca0b7620421457b619151b9', // pragma: allowlist secret + version: version, + }, + }; + } + return newManifest; +} diff --git a/packages/tools/helpers/src/generateLocalPackageMetadata.ts b/packages/tools/helpers/src/generateLocalPackageMetadata.ts new file mode 100644 index 000000000..eba36fd7f --- /dev/null +++ b/packages/tools/helpers/src/generateLocalPackageMetadata.ts @@ -0,0 +1,67 @@ +import { GenericBody, Manifest } from '@verdaccio/types'; + +import { getTarball } from './utils'; + +export function generateLocalPackageMetadata( + pkgName: string, + version = '1.0.0', + domain: string = 'http://localhost:5555', + time?: GenericBody +): Manifest { + // @ts-ignore + return { + _id: pkgName, + name: pkgName, + description: '', + 'dist-tags': { ['latest']: version }, + versions: { + [version]: { + name: pkgName, + version: version, + description: 'package generated', + main: 'index.js', + scripts: { + test: 'echo "Error: no test specified" && exit 1', + }, + keywords: [], + author: { + name: 'User NPM', + email: 'user@domain.com', + }, + license: 'ISC', + dependencies: { + verdaccio: '^2.7.2', + }, + readme: '# test', + readmeFilename: 'README.md', + _id: `${pkgName}@${version}`, + _npmVersion: '5.5.1', + _npmUser: { + name: 'foo', + }, + dist: { + integrity: + 'sha512-6gHiERpiDgtb3hjqpQH5/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cm' + + 'E6dUBf+XoPoH4g==', + shasum: '2c03764f651a9f016ca0b7620421457b619151b9', // pragma: allowlist secret + tarball: `${domain}/${pkgName}\/-\/${getTarball(pkgName)}-${version}.tgz`, + }, + }, + }, + time: time ?? { + modified: new Date().toISOString(), + created: new Date().toISOString(), + [version]: new Date().toISOString(), + }, + readme: '# test', + _attachments: { + [`${getTarball(pkgName)}-${version}.tgz`]: { + shasum: '2c03764f651a9f016ca0b7620421457b619151b9', // pragma: allowlist secret + version: version, + }, + }, + _uplinks: {}, + _distfiles: {}, + _rev: '', + }; +} diff --git a/packages/tools/helpers/src/generatePackageMetadata.ts b/packages/tools/helpers/src/generatePackageMetadata.ts index 1cc8607c4..4c7ddc5f3 100644 --- a/packages/tools/helpers/src/generatePackageMetadata.ts +++ b/packages/tools/helpers/src/generatePackageMetadata.ts @@ -1,236 +1,7 @@ -import { FullRemoteManifest, GenericBody, Manifest, Version, Versions } from '@verdaccio/types'; +import { Manifest } from '@verdaccio/types'; -export interface DistTags { - [key: string]: string; -} - -const getTarball = (name: string): string => { - const r = name.split('/'); - if (r.length === 1) { - return r[0]; - } else { - return r[1]; - } -}; - -export function addNewVersion( - manifest: Manifest, - version: string, - isRemote: boolean = true, - domain: string = 'http://localhost:5555' -): Manifest { - const currentVersions = Object.keys(manifest.versions); - if (currentVersions.includes(version)) { - throw new Error(`Version ${version} already exists`); - } - - const newManifest = { ...manifest }; - newManifest.versions[version] = { - name: manifest.name, - version, - description: manifest.description ?? '', - readme: '', - main: 'index.js', - scripts: { test: 'echo "Error: no test specified" && exit 1' }, - keywords: [], - author: { name: 'User NPM', email: 'user@domain.com' }, - license: 'ISC', - dependencies: { verdaccio: '^2.7.2' }, - readmeFilename: 'README.md', - _id: `${manifest.name}@${version}`, - _npmVersion: '5.5.1', - _npmUser: { name: 'foo' }, - dist: { - integrity: 'sha512-6gHiERpiDgtb3hjqpQHoPoH4g==', - shasum: '2c03764f651a9f016ca0b7620421457b619151b9', - tarball: `${domain}/${manifest.name}/-/${getTarball(manifest.name)}-${version}.tgz`, - }, - contributors: [], - }; - // update the latest with the new version - newManifest['dist-tags'] = { latest: version }; - // add new version does not need attachments - if (isRemote) { - newManifest._distfiles = { - ...newManifest._distfiles, - [`${getTarball(manifest.name)}-${version}.tgz`]: { - sha: '2c03764f651a9f016ca0b7620421457b619151b9', - url: `${domain}/${manifest.name}/-/${getTarball(manifest.name)}-${version}.tgz`, - }, - }; - } else { - newManifest._attachments = { - ...newManifest._attachments, - [`${getTarball(manifest.name)}-${version}.tgz`]: { - shasum: '2c03764f651a9f016ca0b7620421457b619151b9', // pragma: allowlist secret - version: version, - }, - }; - } - return newManifest; -} - -export function generateLocalPackageMetadata( - pkgName: string, - version = '1.0.0', - domain: string = 'http://localhost:5555', - time?: GenericBody -): Manifest { - // @ts-ignore - return { - _id: pkgName, - name: pkgName, - description: '', - 'dist-tags': { ['latest']: version }, - versions: { - [version]: { - name: pkgName, - version: version, - description: 'package generated', - main: 'index.js', - scripts: { - test: 'echo "Error: no test specified" && exit 1', - }, - keywords: [], - author: { - name: 'User NPM', - email: 'user@domain.com', - }, - license: 'ISC', - dependencies: { - verdaccio: '^2.7.2', - }, - readme: '# test', - readmeFilename: 'README.md', - _id: `${pkgName}@${version}`, - _npmVersion: '5.5.1', - _npmUser: { - name: 'foo', - }, - dist: { - integrity: - 'sha512-6gHiERpiDgtb3hjqpQH5/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cm' + - 'E6dUBf+XoPoH4g==', - shasum: '2c03764f651a9f016ca0b7620421457b619151b9', // pragma: allowlist secret - tarball: `${domain}/${pkgName}\/-\/${getTarball(pkgName)}-${version}.tgz`, - }, - }, - }, - time: time ?? { - modified: new Date().toISOString(), - created: new Date().toISOString(), - [version]: new Date().toISOString(), - }, - readme: '# test', - _attachments: { - [`${getTarball(pkgName)}-${version}.tgz`]: { - shasum: '2c03764f651a9f016ca0b7620421457b619151b9', // pragma: allowlist secret - version: version, - }, - }, - _uplinks: {}, - _distfiles: {}, - _rev: '', - }; -} - -export function generateRemotePackageMetadata( - pkgName: string, - version = '1.0.0', - domain: string = 'http://localhost:5555', - versions: string[] = [] -): FullRemoteManifest { - // @ts-ignore - const generateVersion = (version: string): Version => { - const metadata = { - name: pkgName, - version: version, - description: 'package generated', - main: 'index.js', - scripts: { - test: 'echo "Error: no test specified" && exit 1', - }, - keywords: [], - author: { - name: 'User NPM', - email: 'user@domain.com', - }, - license: 'ISC', - dependencies: { - verdaccio: '^2.7.2', - }, - readme: '# test', - readmeFilename: 'README.md', - _id: `${pkgName}@${version}`, - _npmVersion: '5.5.1', - _npmUser: { - name: 'foo', - }, - dist: { - integrity: - 'sha512-6gHiERpiDgtb3hjqpQH5/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cm' + - 'E6dUBf+XoPoH4g==', - shasum: '2c03764f651a9f016ca0b7620421457b619151b9', // pragma: allowlist secret - tarball: `${domain}\/${pkgName}\/-\/${getTarball(pkgName)}-${version}.tgz`, - }, - }; - - return metadata; - }; - const mappedVersions: Versions = versions.reduce((acc, v) => { - acc[v] = generateVersion(v); - return acc; - }, {}); - - const mappedTimes: GenericBody = versions.reduce((acc, v) => { - const date = new Date(Date.now()); - acc[v] = date.toISOString(); - return acc; - }, {}); - - return { - _id: pkgName, - name: pkgName, - description: '', - 'dist-tags': { ['latest']: version }, - versions: { - [version]: generateVersion(version), - ...mappedVersions, - }, - time: { - modified: '2019-06-13T06:44:45.747Z', - created: '2019-06-13T06:44:45.747Z', - [version]: '2019-06-13T06:44:45.747Z', - ...mappedTimes, - }, - maintainers: [ - { - name: 'foo', - email: 'foo@foo.com', - }, - ], - author: { - name: 'foo', - }, - readme: '# test', - _rev: '12-c8fe8a9c79fa57a87347a0213e6f2548', - }; -} - -export function getDeprecatedPackageMetadata( - pkgName: string, - version = '1.0.0', - distTags: DistTags = { ['latest']: version }, - deprecated = 'default deprecated message', - rev = 'rev-foo' -): Manifest { - const manifest = generatePackageMetadata(pkgName, version, distTags); - // deprecated message requires empty attachments - manifest._attachments = {}; - manifest._rev = rev; - manifest.versions[version].deprecated = deprecated; - return manifest; -} +import { DistTags } from './types'; +import { getTarball } from './utils'; export function generatePackageMetadata( pkgName: string, diff --git a/packages/tools/helpers/src/generateRemotePackageMetadata.ts b/packages/tools/helpers/src/generateRemotePackageMetadata.ts new file mode 100644 index 000000000..26fb267cf --- /dev/null +++ b/packages/tools/helpers/src/generateRemotePackageMetadata.ts @@ -0,0 +1,86 @@ +import { FullRemoteManifest, GenericBody, Version, Versions } from '@verdaccio/types'; + +import { getTarball } from './utils'; + +export function generateRemotePackageMetadata( + pkgName: string, + version = '1.0.0', + domain: string = 'http://localhost:5555', + versions: string[] = [] +): FullRemoteManifest { + // @ts-ignore + const generateVersion = (version: string): Version => { + const metadata = { + name: pkgName, + version: version, + description: 'package generated', + main: 'index.js', + scripts: { + test: 'echo "Error: no test specified" && exit 1', + }, + keywords: [], + author: { + name: 'User NPM', + email: 'user@domain.com', + }, + license: 'ISC', + dependencies: { + verdaccio: '^2.7.2', + }, + readme: '# test', + readmeFilename: 'README.md', + _id: `${pkgName}@${version}`, + _npmVersion: '5.5.1', + _npmUser: { + name: 'foo', + }, + dist: { + integrity: + 'sha512-6gHiERpiDgtb3hjqpQH5/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cm' + + 'E6dUBf+XoPoH4g==', + shasum: '2c03764f651a9f016ca0b7620421457b619151b9', // pragma: allowlist secret + tarball: `${domain}\/${pkgName}\/-\/${getTarball(pkgName)}-${version}.tgz`, + }, + }; + + return metadata; + }; + const mappedVersions: Versions = versions.reduce((acc, v) => { + acc[v] = generateVersion(v); + return acc; + }, {}); + + const mappedTimes: GenericBody = versions.reduce((acc, v) => { + const date = new Date(Date.now()); + acc[v] = date.toISOString(); + return acc; + }, {}); + + return { + _id: pkgName, + name: pkgName, + description: '', + 'dist-tags': { ['latest']: version }, + versions: { + [version]: generateVersion(version), + ...mappedVersions, + }, + time: { + modified: '2019-06-13T06:44:45.747Z', + created: '2019-06-13T06:44:45.747Z', + [version]: '2019-06-13T06:44:45.747Z', + ...mappedTimes, + }, + maintainers: [ + { + name: 'foo', + email: 'foo@foo.com', + }, + ], + author: { + name: 'foo', + }, + readme: '# test', + _rev: '12-c8fe8a9c79fa57a87347a0213e6f2548', + }; +} diff --git a/packages/tools/helpers/src/getDeprecatedPackageMetadata.ts b/packages/tools/helpers/src/getDeprecatedPackageMetadata.ts new file mode 100644 index 000000000..d7c223ab5 --- /dev/null +++ b/packages/tools/helpers/src/getDeprecatedPackageMetadata.ts @@ -0,0 +1,19 @@ +import { Manifest } from '@verdaccio/types'; + +import { generatePackageMetadata } from './generatePackageMetadata'; +import { DistTags } from './types'; + +export function getDeprecatedPackageMetadata( + pkgName: string, + version = '1.0.0', + distTags: DistTags = { ['latest']: version }, + deprecated = 'default deprecated message', + rev = 'rev-foo' +): Manifest { + const manifest = generatePackageMetadata(pkgName, version, distTags); + // deprecated message requires empty attachments + manifest._attachments = {}; + manifest._rev = rev; + manifest.versions[version].deprecated = deprecated; + return manifest; +} diff --git a/packages/tools/helpers/src/index.ts b/packages/tools/helpers/src/index.ts index afd88b043..0e8670bce 100644 --- a/packages/tools/helpers/src/index.ts +++ b/packages/tools/helpers/src/index.ts @@ -1,10 +1,8 @@ -export { - generatePackageMetadata, - addNewVersion, - generateLocalPackageMetadata, - generateRemotePackageMetadata, - getDeprecatedPackageMetadata, -} from './generatePackageMetadata'; +export { generatePackageMetadata } from './generatePackageMetadata'; +export { getDeprecatedPackageMetadata } from './getDeprecatedPackageMetadata'; +export { generateLocalPackageMetadata } from './generateLocalPackageMetadata'; +export { generateRemotePackageMetadata } from './generateRemotePackageMetadata'; +export { addNewVersion } from './addNewVersion'; export { generatePublishNewVersionManifest } from './generatePublishNewVersionManifest'; export { initializeServer } from './initializeServer'; export { publishVersion } from './actions'; diff --git a/packages/tools/helpers/src/types.ts b/packages/tools/helpers/src/types.ts new file mode 100644 index 000000000..26a60187a --- /dev/null +++ b/packages/tools/helpers/src/types.ts @@ -0,0 +1,3 @@ +export interface DistTags { + [key: string]: string; +} diff --git a/packages/tools/helpers/src/utils.ts b/packages/tools/helpers/src/utils.ts index 0d7968f21..5ce07eade 100644 --- a/packages/tools/helpers/src/utils.ts +++ b/packages/tools/helpers/src/utils.ts @@ -11,3 +11,12 @@ import path from 'path'; export function createTempFolder(prefix: string): string { return fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), prefix)); } + +export const getTarball = (name: string): string => { + const r = name.split('/'); + if (r.length === 1) { + return r[0]; + } else { + return r[1]; + } +}; diff --git a/packages/tools/helpers/tests/metadata.spec.ts b/packages/tools/helpers/tests/metadata.spec.ts index f131148d9..6676e4d6e 100644 --- a/packages/tools/helpers/tests/metadata.spec.ts +++ b/packages/tools/helpers/tests/metadata.spec.ts @@ -1,8 +1,9 @@ -import { addNewVersion, generatePackageMetadata } from '../src'; import { + addNewVersion, generateLocalPackageMetadata, + generatePackageMetadata, generateRemotePackageMetadata, -} from '../src/generatePackageMetadata'; +} from '../src'; describe('generate metadata', () => { describe('generatePackageMetadata', () => { diff --git a/packages/verdaccio/src/server/request.ts b/packages/verdaccio/src/server/request.ts index 8765ba240..5794a7a84 100644 --- a/packages/verdaccio/src/server/request.ts +++ b/packages/verdaccio/src/server/request.ts @@ -275,10 +275,14 @@ export class ServerQuery { }); } - public async addPackage(name: string, version: string = '1.0.0'): Promise { + public async addPackage( + name: string, + version: string = '1.0.0', + message = API_MESSAGE.PKG_CREATED + ): Promise { return (await this.putPackage(name, generatePackageMetadata(name, version))) .status(HTTP_STATUS.CREATED) - .body_ok(API_MESSAGE.PKG_CREATED); + .body_ok(message); } public async addPackageAssert(name: string, version: string = '1.0.0'): Promise { diff --git a/packages/verdaccio/test/basic.spec.ts b/packages/verdaccio/test/basic.spec.ts index 27ca55a47..a6f85ac71 100644 --- a/packages/verdaccio/test/basic.spec.ts +++ b/packages/verdaccio/test/basic.spec.ts @@ -1,5 +1,5 @@ import { ConfigBuilder } from '@verdaccio/config'; -import { HTTP_STATUS, constants, fileUtils } from '@verdaccio/core'; +import { API_MESSAGE, HTTP_STATUS, constants, fileUtils } from '@verdaccio/core'; import { Registry, ServerQuery } from '../src/server'; @@ -41,8 +41,8 @@ describe('basic test endpoints', () => { test('shoud unpublish the whole package of many published', async function () { const server = new ServerQuery(registry.getRegistryUrl()); await server.addPackage('unpublish-new-package', '1.0.0'); - await server.addPackage('unpublish-new-package', '1.0.1'); - await server.addPackage('unpublish-new-package', '1.0.2'); + await server.addPackage('unpublish-new-package', '1.0.1', API_MESSAGE.PKG_CHANGED); + await server.addPackage('unpublish-new-package', '1.0.2', API_MESSAGE.PKG_CHANGED); (await server.getPackage('unpublish-new-package')).status(HTTP_STATUS.OK); (await server.removePackage('unpublish-new-package', '_rev')).status(HTTP_STATUS.CREATED); (await server.getPackage('unpublish-new-package')).status(HTTP_STATUS.NOT_FOUND); diff --git a/packages/verdaccio/test/massive.spec.ts b/packages/verdaccio/test/massive.spec.ts index 3391cd4c8..0df35a46a 100644 --- a/packages/verdaccio/test/massive.spec.ts +++ b/packages/verdaccio/test/massive.spec.ts @@ -1,5 +1,5 @@ import { ConfigBuilder } from '@verdaccio/config'; -import { constants, fileUtils } from '@verdaccio/core'; +import { API_MESSAGE, constants, fileUtils } from '@verdaccio/core'; import { Registry, ServerQuery } from '../src/server'; @@ -37,12 +37,13 @@ describe('race publishing packages', () => { for (const time of Array.from(Array(times).keys())) { try { - await server.addPackage('race-pkg', `1.0.${time}`); + let message = success === 0 ? API_MESSAGE.PKG_CREATED : API_MESSAGE.PKG_CHANGED; + await server.addPackage('race-pkg', `1.0.${time}`, message); success++; } catch (error) { console.error('this should not trigger', error); } } expect(success).toBe(times); - }, 30000); + }, 40000); });