0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-16 21:56:25 -05:00

refactor: reimplement star command (#3410)

This commit is contained in:
Juan Picado 2022-10-01 00:14:20 +02:00 committed by GitHub
parent 6ad13de884
commit ce013d2fcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1086 additions and 354 deletions

View file

@ -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

View file

@ -19,6 +19,7 @@
| deprecate | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ |
| ping | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ |
| search | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ |
| star | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⛔ | ⛔ | ⛔ | ⛔ |
| dist-tag | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
> notes:

View file

@ -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();
});
});

View file

@ -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();
});
});

View file

@ -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();
});
});

View file

@ -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();
});
});

View file

@ -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();
});
});

View file

@ -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();
});
});

View file

@ -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<void> {
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

View file

@ -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);
}
);
});

View file

@ -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 {

View file

@ -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,
},
},
});

View file

@ -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 {

View file

@ -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<void> {
if (isDeprecatedManifest(manifest)) {
public async updateManifest(
manifest: Manifest | StarManifestBody,
options: UpdateManifestOptions
): Promise<string | undefined> {
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<void> {
// // const storage: IPackageStorage = this.getPrivatePackageStorage(opname);
private async star(manifest: StarManifestBody, options: UpdateManifestOptions): Promise<string> {
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

View file

@ -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<Config>[];
/**
* When the command `npm star` is executed, the body only contains the following
* values in the body.
*/
export type StarManifestBody = Pick<Manifest, '_id' | 'users' | '_rev'>;

View file

@ -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);
});
});
});

View file

@ -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);
});
});
});

View file

@ -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', () => {

View file

@ -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;
}

View file

@ -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: '',
};
}

View file

@ -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,

View file

@ -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',
};
}

View file

@ -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;
}

View file

@ -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';

View file

@ -0,0 +1,3 @@
export interface DistTags {
[key: string]: string;
}

View file

@ -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];
}
};

View file

@ -1,8 +1,9 @@
import { addNewVersion, generatePackageMetadata } from '../src';
import {
addNewVersion,
generateLocalPackageMetadata,
generatePackageMetadata,
generateRemotePackageMetadata,
} from '../src/generatePackageMetadata';
} from '../src';
describe('generate metadata', () => {
describe('generatePackageMetadata', () => {

View file

@ -275,10 +275,14 @@ export class ServerQuery {
});
}
public async addPackage(name: string, version: string = '1.0.0'): Promise<ResponseAssert> {
public async addPackage(
name: string,
version: string = '1.0.0',
message = API_MESSAGE.PKG_CREATED
): Promise<ResponseAssert> {
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<ResponseAssert> {

View file

@ -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);

View file

@ -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);
});