mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-24 21:15:51 +01:00
refactor: migrate request to node-fetch at hooks package (#1946)
* refactor(hooks): new structure for notifications * chore: fix build * chore: add debug * chore: add changeset
This commit is contained in:
parent
8c730c0694
commit
65cb26cf31
4
.babelrc
4
.babelrc
@ -2,6 +2,10 @@
|
||||
"presets": [ [
|
||||
"@babel/env",
|
||||
{
|
||||
"useBuiltIns": "usage",
|
||||
"corejs": {
|
||||
"version": 3, "proposals": true
|
||||
},
|
||||
"targets": {
|
||||
"node": 10
|
||||
}
|
||||
|
6
.changeset/late-parents-act.md
Normal file
6
.changeset/late-parents-act.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
'@verdaccio/hooks': patch
|
||||
'@verdaccio/proxy': patch
|
||||
---
|
||||
|
||||
refactor: migrate request to node-fetch at hooks package
|
@ -1,4 +1,4 @@
|
||||
FROM node:12.18.3-alpine as builder
|
||||
FROM node:12.18.4-alpine as builder
|
||||
|
||||
ENV NODE_ENV=development \
|
||||
VERDACCIO_BUILD_REGISTRY=https://registry.verdaccio.org
|
||||
@ -12,14 +12,14 @@ RUN apk --no-cache add openssl ca-certificates wget && \
|
||||
WORKDIR /opt/verdaccio-build
|
||||
COPY . .
|
||||
|
||||
RUN npm -g i pnpm@latest && \
|
||||
RUN npm -g i pnpm@5.5.12 && \
|
||||
pnpm config set registry $VERDACCIO_BUILD_REGISTRY && \
|
||||
pnpm recursive install --frozen-lockfile --ignore-scripts && \
|
||||
pnpm run build && \
|
||||
pnpm run lint && \
|
||||
pnpm install --prod --ignore-scripts
|
||||
|
||||
FROM node:12.18.3-alpine
|
||||
FROM node:12.18.4-alpine
|
||||
LABEL maintainer="https://github.com/verdaccio/verdaccio"
|
||||
|
||||
ENV VERDACCIO_APPDIR=/opt/verdaccio \
|
||||
|
@ -28,6 +28,7 @@
|
||||
"@verdaccio/loaders": "workspace:5.0.0-alpha.0",
|
||||
"@verdaccio/logger": "workspace:5.0.0-alpha.0",
|
||||
"@verdaccio/utils": "workspace:5.0.0-alpha.0",
|
||||
"@verdaccio/auth": "workspace:5.0.0-alpha.0",
|
||||
"debug": "^4.1.1",
|
||||
"express": "4.17.1",
|
||||
"lodash": "4.17.15"
|
||||
|
@ -15,7 +15,7 @@
|
||||
"homepage": "https://verdaccio.org",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@verdaccio/types": "workspace:10.0.0-beta"
|
||||
"@verdaccio/types": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build",
|
||||
|
@ -23,7 +23,7 @@
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@verdaccio/types": "^9.7.2"
|
||||
"@verdaccio/types": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build",
|
||||
|
5
packages/core/types/index.d.ts
vendored
5
packages/core/types/index.d.ts
vendored
@ -218,6 +218,7 @@ declare module '@verdaccio/types' {
|
||||
max_users: number;
|
||||
}
|
||||
|
||||
// FUTURE: rename to Notification
|
||||
interface Notifications {
|
||||
method: string;
|
||||
packagePattern: RegExp;
|
||||
@ -227,6 +228,8 @@ declare module '@verdaccio/types' {
|
||||
headers: Headers;
|
||||
}
|
||||
|
||||
type Notification = Notifications;
|
||||
|
||||
interface ConfigFile {
|
||||
storage: string;
|
||||
plugins: string;
|
||||
@ -364,7 +367,9 @@ declare module '@verdaccio/types' {
|
||||
https_proxy?: string;
|
||||
no_proxy?: string;
|
||||
max_body_size?: string;
|
||||
// deprecated
|
||||
notifications?: Notifications;
|
||||
notify?: Notifications | Notifications[];
|
||||
middlewares?: any;
|
||||
filters?: any;
|
||||
checkSecretKey(token: string): string;
|
||||
|
@ -15,18 +15,20 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://verdaccio.org",
|
||||
"dependencies": {
|
||||
"debug": "^4.2.0",
|
||||
"@verdaccio/commons-api": "workspace:*",
|
||||
"@verdaccio/logger": "workspace:5.0.0-alpha.0",
|
||||
"handlebars": "4.5.3",
|
||||
"lodash": "^4.17.20",
|
||||
"request": "2.87.0"
|
||||
"request": "2.87.0",
|
||||
"node-fetch": "^2.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@verdaccio/auth": "workspace:5.0.0-alpha.0",
|
||||
"@verdaccio/config": "workspace:5.0.0-alpha.0",
|
||||
"@verdaccio/dev-commons": "workspace:5.0.0-alpha.0",
|
||||
"@verdaccio/types": "workspace:*",
|
||||
"@verdaccio/utils": "workspace:5.0.0-alpha.0"
|
||||
"@verdaccio/utils": "workspace:5.0.0-alpha.0",
|
||||
"nock": "^13.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build",
|
||||
|
@ -1,23 +1,38 @@
|
||||
import isNil from 'lodash/isNil';
|
||||
import request, { RequiredUriUrl } from 'request';
|
||||
import fetch, { RequestInit } from 'node-fetch';
|
||||
import buildDebug from 'debug';
|
||||
|
||||
import { logger } from '@verdaccio/logger';
|
||||
import { HTTP_STATUS } from '@verdaccio/commons-api';
|
||||
|
||||
export function notifyRequest(options: RequiredUriUrl, content): Promise<any | Error> {
|
||||
return new Promise((resolve, reject): void => {
|
||||
request(options, function (err, response, body): void {
|
||||
if (err || response.statusCode >= HTTP_STATUS.BAD_REQUEST) {
|
||||
const errorMessage = isNil(err) ? response.body : err.message;
|
||||
logger.error({ errorMessage }, 'notify service has thrown an error: @{errorMessage}');
|
||||
reject(errorMessage);
|
||||
}
|
||||
logger.info({ content }, 'A notification has been shipped: @{content}');
|
||||
if (isNil(body) === false) {
|
||||
logger.debug({ body }, ' body: @{body}');
|
||||
resolve(body);
|
||||
}
|
||||
reject(Error('body is missing'));
|
||||
const debug = buildDebug('verdaccio:hooks:request');
|
||||
export type NotifyRequestOptions = RequestInit;
|
||||
|
||||
export async function notifyRequest(url: string, options: NotifyRequestOptions): Promise<boolean> {
|
||||
let response;
|
||||
try {
|
||||
debug('uri %o', url);
|
||||
response = await fetch(url, {
|
||||
body: JSON.stringify(options.body),
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
});
|
||||
debug('response.status %o', response.status);
|
||||
const body = await response.json();
|
||||
if (response.status >= HTTP_STATUS.BAD_REQUEST) {
|
||||
throw new Error(body);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ content: options.body },
|
||||
'The notification @{content} has been successfully dispatched'
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
debug('request error %o', err);
|
||||
logger.error(
|
||||
{ errorMessage: err?.message },
|
||||
'notify service has thrown an error: @{errorMessage}'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +1,62 @@
|
||||
import Handlebars from 'handlebars';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { OptionsWithUrl } from 'request';
|
||||
import { Config, Package, RemoteUser } from '@verdaccio/types';
|
||||
import { notifyRequest } from './notify-request';
|
||||
import buildDebug from 'debug';
|
||||
import { Config, Package, RemoteUser, Notification } from '@verdaccio/types';
|
||||
import { logger } from '@verdaccio/logger';
|
||||
import { notifyRequest, NotifyRequestOptions } from './notify-request';
|
||||
|
||||
const debug = buildDebug('verdaccio:hooks');
|
||||
type TemplateMetadata = Package & { publishedPackage: string };
|
||||
|
||||
export function handleNotify(
|
||||
metadata: Package,
|
||||
export function compileTemplate(content, metadata) {
|
||||
// FUTURE: multiple handlers
|
||||
return new Promise((resolve, reject) => {
|
||||
let handler;
|
||||
try {
|
||||
if (!handler) {
|
||||
debug('compile default template handler %o', content);
|
||||
const template: HandlebarsTemplateDelegate = Handlebars.compile(content);
|
||||
return resolve(template(metadata));
|
||||
}
|
||||
} catch (error) {
|
||||
debug('error template handler %o', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleNotify(
|
||||
metadata: Partial<Package>,
|
||||
notifyEntry,
|
||||
remoteUser: RemoteUser,
|
||||
remoteUser: Partial<RemoteUser>,
|
||||
publishedPackage: string
|
||||
): Promise<any> | void {
|
||||
): Promise<boolean> {
|
||||
let regex;
|
||||
if (metadata.name && notifyEntry.packagePattern) {
|
||||
regex = new RegExp(notifyEntry.packagePattern, notifyEntry.packagePatternFlags || '');
|
||||
if (!regex.test(metadata.name)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const template: HandlebarsTemplateDelegate = Handlebars.compile(notifyEntry.content);
|
||||
// don't override 'publisher' if package.json already has that
|
||||
/* eslint no-unused-vars: 0 */
|
||||
/* eslint @typescript-eslint/no-unused-vars: 0 */
|
||||
let content;
|
||||
// FIXME: publisher is not part of the expected types metadata
|
||||
// @ts-ignore
|
||||
if (_.isNil(metadata.publisher)) {
|
||||
if (typeof metadata?.publisher === 'undefined' || metadata?.publisher === null) {
|
||||
// @ts-ignore
|
||||
metadata = { ...metadata, publishedPackage, publisher: { name: remoteUser.name as string } };
|
||||
debug('template metadata %o', metadata);
|
||||
content = await compileTemplate(notifyEntry.content, metadata);
|
||||
}
|
||||
|
||||
const content: string = template(metadata);
|
||||
|
||||
const options: OptionsWithUrl = {
|
||||
body: content,
|
||||
url: '',
|
||||
const options: NotifyRequestOptions = {
|
||||
body: JSON.stringify(content),
|
||||
};
|
||||
|
||||
// provides fallback support, it's accept an Object {} and Array of {}
|
||||
if (notifyEntry.headers && _.isArray(notifyEntry.headers)) {
|
||||
if (notifyEntry.headers && Array.isArray(notifyEntry.headers)) {
|
||||
const header = {};
|
||||
// FIXME: we can simplify this
|
||||
notifyEntry.headers.map(function (item): void {
|
||||
if (Object.is(item, item)) {
|
||||
for (const key in item) {
|
||||
@ -56,44 +72,67 @@ export function handleNotify(
|
||||
options.headers = notifyEntry.headers;
|
||||
}
|
||||
|
||||
options.method = notifyEntry.method;
|
||||
|
||||
if (notifyEntry.endpoint) {
|
||||
options.url = notifyEntry.endpoint;
|
||||
if (!notifyEntry.endpoint) {
|
||||
debug('error due endpoint is missing');
|
||||
throw new Error('missing parameter');
|
||||
}
|
||||
|
||||
return notifyRequest(options, content);
|
||||
return notifyRequest(notifyEntry.endpoint, {
|
||||
method: notifyEntry.method,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function sendNotification(
|
||||
metadata: Package,
|
||||
metadata: Partial<Package>,
|
||||
notify: Notification,
|
||||
remoteUser: RemoteUser,
|
||||
remoteUser: Partial<RemoteUser>,
|
||||
publishedPackage: string
|
||||
): Promise<any> {
|
||||
): Promise<boolean> {
|
||||
return handleNotify(metadata, notify, remoteUser, publishedPackage) as Promise<any>;
|
||||
}
|
||||
|
||||
export function notify(
|
||||
metadata: Package,
|
||||
config: Config,
|
||||
remoteUser: RemoteUser,
|
||||
export async function notify(
|
||||
metadata: Partial<Package>,
|
||||
config: Partial<Config>,
|
||||
remoteUser: Partial<RemoteUser>,
|
||||
publishedPackage: string
|
||||
): Promise<any> | void {
|
||||
): Promise<boolean[]> {
|
||||
debug('init send notification');
|
||||
if (config.notify) {
|
||||
if (config.notify.content) {
|
||||
return sendNotification(
|
||||
metadata,
|
||||
(config.notify as unknown) as Notification,
|
||||
remoteUser,
|
||||
publishedPackage
|
||||
);
|
||||
}
|
||||
// multiple notifications endpoints PR #108
|
||||
return Promise.all(
|
||||
_.map(config.notify, (key) => sendNotification(metadata, key, remoteUser, publishedPackage))
|
||||
);
|
||||
}
|
||||
const isSingle = Object.keys(config.notify).includes('method');
|
||||
if (isSingle) {
|
||||
debug('send single notification');
|
||||
try {
|
||||
const response = await sendNotification(
|
||||
metadata,
|
||||
config.notify as Notification,
|
||||
remoteUser,
|
||||
publishedPackage
|
||||
);
|
||||
return [response];
|
||||
} catch {
|
||||
debug('error on sending single notification');
|
||||
return [false];
|
||||
}
|
||||
} else {
|
||||
debug('send multiples notification');
|
||||
const results = await Promise.allSettled(
|
||||
Object.keys(config.notify).map((keyId: string) => {
|
||||
// @ts-ignore
|
||||
const item = config.notify[keyId];
|
||||
debug('send item %o', item);
|
||||
return sendNotification(metadata, item, remoteUser, publishedPackage);
|
||||
})
|
||||
).catch((error) => {
|
||||
logger.error({ error }, 'notify request has failed: @error');
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
// @ts-ignore
|
||||
return Object.keys(results).map((promiseValue) => results[promiseValue].value);
|
||||
}
|
||||
} else {
|
||||
debug('no notifications configuration detected');
|
||||
return [false];
|
||||
}
|
||||
}
|
||||
|
85
packages/hooks/test/notify-request.spec.ts
Normal file
85
packages/hooks/test/notify-request.spec.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import nock from 'nock';
|
||||
import { createRemoteUser, parseConfigFile } from '@verdaccio/utils';
|
||||
import { Config } from '@verdaccio/types';
|
||||
import { notify } from '../src/notify';
|
||||
import { parseConfigurationFile } from './__helper';
|
||||
|
||||
const parseConfigurationNotifyFile = (name) => {
|
||||
return parseConfigurationFile(`notify/${name}`);
|
||||
};
|
||||
const singleNotificationConfig = parseConfigFile(parseConfigurationNotifyFile('single.notify'));
|
||||
const singleHeaderNotificationConfig = parseConfigFile(
|
||||
parseConfigurationNotifyFile('single.header.notify')
|
||||
);
|
||||
const packagePatternNotificationConfig = parseConfigFile(
|
||||
parseConfigurationNotifyFile('single.packagePattern.notify')
|
||||
);
|
||||
const multiNotificationConfig = parseConfigFile(parseConfigurationNotifyFile('multiple.notify'));
|
||||
|
||||
const mockInfo = jest.fn();
|
||||
jest.mock('@verdaccio/logger', () => ({
|
||||
setup: jest.fn(),
|
||||
logger: {
|
||||
child: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: () => mockInfo(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const domain = 'http://slack-service';
|
||||
|
||||
describe('Notifications:: notifyRequest', () => {
|
||||
beforeEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
test('when sending a empty notification', async () => {
|
||||
nock(domain).post('/foo?auth_token=mySecretToken').reply(200, { body: 'test' });
|
||||
|
||||
const notificationResponse = await notify({}, {}, createRemoteUser('foo', []), 'bar');
|
||||
expect(notificationResponse).toEqual([false]);
|
||||
});
|
||||
|
||||
test('when sending a single notification', async () => {
|
||||
nock(domain).post('/foo?auth_token=mySecretToken').reply(200, { body: 'test' });
|
||||
|
||||
const notificationResponse = await notify(
|
||||
{},
|
||||
singleHeaderNotificationConfig,
|
||||
createRemoteUser('foo', []),
|
||||
'bar'
|
||||
);
|
||||
expect(notificationResponse).toEqual([true]);
|
||||
});
|
||||
|
||||
test('when notification endpoint is missing', async () => {
|
||||
nock(domain).post('/foo?auth_token=mySecretToken').reply(200, { body: 'test' });
|
||||
const name = 'package';
|
||||
const config: Partial<Config> = {
|
||||
// @ts-ignore
|
||||
notify: {
|
||||
method: 'POST',
|
||||
endpoint: undefined,
|
||||
content: '',
|
||||
},
|
||||
};
|
||||
const notificationResponse = await notify({ name }, config, createRemoteUser('foo', []), 'bar');
|
||||
expect(notificationResponse).toEqual([false]);
|
||||
});
|
||||
|
||||
test('when multiple notifications', async () => {
|
||||
nock(domain).post('/foo?auth_token=mySecretToken').reply(200, { body: 'test' });
|
||||
nock(domain).post('/foo?auth_token=mySecretToken').reply(400, {});
|
||||
nock(domain)
|
||||
.post('/foo?auth_token=mySecretToken')
|
||||
.reply(500, { message: 'Something bad happened' });
|
||||
|
||||
const name = 'package';
|
||||
const responses = await notify({ name }, multiNotificationConfig, { name: 'foo' }, 'bar');
|
||||
expect(responses).toEqual([true, false, false]);
|
||||
});
|
||||
});
|
@ -1,87 +0,0 @@
|
||||
import { parseConfigFile } from '@verdaccio/utils';
|
||||
import { setup } from '@verdaccio/logger';
|
||||
|
||||
import { notify } from '../src';
|
||||
import { notifyRequest } from '../src/notify-request';
|
||||
import { parseConfigurationFile } from './__helper';
|
||||
|
||||
setup([]);
|
||||
|
||||
jest.mock('../src/notify-request', () => ({
|
||||
notifyRequest: jest.fn((options, content) => Promise.resolve([options, content])),
|
||||
}));
|
||||
|
||||
const parseConfigurationNotifyFile = (name) => {
|
||||
return parseConfigurationFile(`notify/${name}`);
|
||||
};
|
||||
const singleNotificationConfig = parseConfigFile(parseConfigurationNotifyFile('single.notify'));
|
||||
const singleHeaderNotificationConfig = parseConfigFile(
|
||||
parseConfigurationNotifyFile('single.header.notify')
|
||||
);
|
||||
const packagePatternNotificationConfig = parseConfigFile(
|
||||
parseConfigurationNotifyFile('single.packagePattern.notify')
|
||||
);
|
||||
const multiNotificationConfig = parseConfigFile(parseConfigurationNotifyFile('multiple.notify'));
|
||||
|
||||
describe('Notifications:: Notify', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// FUTURE: we should add some sort of health check of all props, (not implemented yet)
|
||||
|
||||
test('should not fails if config is not provided', async () => {
|
||||
// @ts-ignore
|
||||
await notify({}, {});
|
||||
|
||||
expect(notifyRequest).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('should send notification', async () => {
|
||||
const name = 'package';
|
||||
// @ts-ignore
|
||||
const response = await notify({ name }, singleNotificationConfig, { name: 'foo' }, 'bar');
|
||||
const [options, content] = response;
|
||||
|
||||
expect(options.headers).toBeDefined();
|
||||
expect(options.url).toBeDefined();
|
||||
expect(options.body).toBeDefined();
|
||||
expect(content).toMatch(name);
|
||||
expect(response).toBeTruthy();
|
||||
expect(notifyRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should send single header notification', async () => {
|
||||
// @ts-ignore
|
||||
await notify({}, singleHeaderNotificationConfig, { name: 'foo' }, 'bar');
|
||||
|
||||
expect(notifyRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should send multiple notification', async () => {
|
||||
const name = 'package';
|
||||
// @ts-ignore
|
||||
await notify({ name }, multiNotificationConfig, { name: 'foo' }, 'bar');
|
||||
|
||||
expect(notifyRequest).toHaveBeenCalled();
|
||||
expect(notifyRequest).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
describe('packagePatternFlags', () => {
|
||||
test('should send single notification with packagePatternFlags', async () => {
|
||||
const name = 'package';
|
||||
// @ts-ignore
|
||||
await notify({ name }, packagePatternNotificationConfig, { name: 'foo' }, 'bar');
|
||||
|
||||
expect(notifyRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should not match on send single notification with packagePatternFlags', async () => {
|
||||
const name = 'no-mach-name';
|
||||
// @ts-ignore
|
||||
await notify({ name }, packagePatternNotificationConfig, { name: 'foo' }, 'bar');
|
||||
|
||||
expect(notifyRequest).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
@ -2,15 +2,15 @@ notify:
|
||||
'example-google-chat':
|
||||
method: POST
|
||||
headers: [{ 'Content-Type': 'application/json' }]
|
||||
endpoint: https://chat.googleapis.com/v1/spaces/AAAAB_TcJYs/messages?key=myKey&token=myToken
|
||||
endpoint: http://slack-service/foo?auth_token=mySecretToken
|
||||
content: '{"text":"New package published: `{{ name }}{{#each versions}} v{{version}}{{/each}}`"}'
|
||||
'example-hipchat':
|
||||
method: POST
|
||||
headers: [{ 'Content-Type': 'application/json' }]
|
||||
endpoint: https://usagge.hipchat.com/v2/room/3729485/notification?auth_token=mySecretToken
|
||||
endpoint: http://slack-service/foo?auth_token=mySecretToken
|
||||
content: '{"color":"green","message":"New package published: * {{ name }}*","notify":true,"message_format":"text"}'
|
||||
'example-stride':
|
||||
method: POST
|
||||
headers: [{ 'Content-Type': 'application/json' }, { 'authorization': 'Bearer secretToken' }]
|
||||
endpoint: https://api.atlassian.com/site/{cloudId}/conversation/{conversationId}/message
|
||||
endpoint: http://slack-service/foo?auth_token=mySecretToken
|
||||
content: '{"body": {"version": 1,"type": "doc","content": [{"type": "paragraph","content": [{"type": "text","text": "New package published: * {{ name }}* Publisher name: * {{ publisher.name }}"}]}]}}'
|
||||
|
@ -1,5 +1,5 @@
|
||||
notify:
|
||||
method: POST
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
endpoint: https://usagge.hipchat.com/v2/room/3729485/notification?auth_token=mySecretToken
|
||||
endpoint: http://slack-service/foo?auth_token=mySecretToken
|
||||
content: '{"color":"green","message":"New package published: * {{ name }}*","notify":true,"message_format":"text"}'
|
||||
|
@ -1,106 +0,0 @@
|
||||
import { HTTP_STATUS, API_ERROR } from '@verdaccio/dev-commons';
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
/**
|
||||
* Mocks Logger Service
|
||||
*/
|
||||
const logger = {
|
||||
logger: {
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
};
|
||||
jest.doMock('@verdaccio/logger', () => logger);
|
||||
|
||||
/**
|
||||
* Test Data
|
||||
*/
|
||||
const options = {
|
||||
url: 'http://slack-service',
|
||||
};
|
||||
const content = 'Verdaccio@x.x.x successfully published';
|
||||
|
||||
describe('Notifications:: notifyRequest', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
test('when notification service throws error', async () => {
|
||||
jest.doMock('request', () => (options, resolver) => {
|
||||
const response = {
|
||||
statusCode: HTTP_STATUS.BAD_REQUEST,
|
||||
};
|
||||
const error = {
|
||||
message: API_ERROR.BAD_DATA,
|
||||
};
|
||||
resolver(error, response);
|
||||
});
|
||||
|
||||
const notification = require('../src/notify-request');
|
||||
const args = [
|
||||
{ errorMessage: 'bad data' },
|
||||
'notify service has thrown an error: @{errorMessage}',
|
||||
];
|
||||
|
||||
await expect(notification.notifyRequest(options, content)).rejects.toEqual(API_ERROR.BAD_DATA);
|
||||
expect(logger.logger.error).toHaveBeenCalledWith(...args);
|
||||
});
|
||||
|
||||
test('when notification service throws error with null error value', async () => {
|
||||
jest.doMock('request', () => (options, resolver) => {
|
||||
const response = {
|
||||
statusCode: HTTP_STATUS.BAD_REQUEST,
|
||||
body: API_ERROR.BAD_DATA,
|
||||
};
|
||||
|
||||
resolver(null, response);
|
||||
});
|
||||
|
||||
const notification = require('../src/notify-request');
|
||||
const args = [
|
||||
{ errorMessage: 'bad data' },
|
||||
'notify service has thrown an error: @{errorMessage}',
|
||||
];
|
||||
|
||||
await expect(notification.notifyRequest(options, content)).rejects.toEqual(API_ERROR.BAD_DATA);
|
||||
expect(logger.logger.error).toHaveBeenCalledWith(...args);
|
||||
});
|
||||
|
||||
test('when notification is successfully delivered', async () => {
|
||||
jest.doMock('request', () => (options, resolver) => {
|
||||
const response = {
|
||||
statusCode: HTTP_STATUS.OK,
|
||||
body: 'Successfully delivered',
|
||||
};
|
||||
|
||||
resolver(null, response, response.body);
|
||||
});
|
||||
|
||||
const notification = require('../src/notify-request');
|
||||
const infoArgs = [{ content }, 'A notification has been shipped: @{content}'];
|
||||
const debugArgs = [{ body: 'Successfully delivered' }, ' body: @{body}'];
|
||||
|
||||
await expect(notification.notifyRequest(options, content)).resolves.toEqual(
|
||||
'Successfully delivered'
|
||||
);
|
||||
expect(logger.logger.info).toHaveBeenCalledWith(...infoArgs);
|
||||
expect(logger.logger.debug).toHaveBeenCalledWith(...debugArgs);
|
||||
});
|
||||
|
||||
test('when notification is successfully delivered but body is undefined/null', async () => {
|
||||
jest.doMock('request', () => (options, resolver) => {
|
||||
const response = {
|
||||
statusCode: HTTP_STATUS.OK,
|
||||
};
|
||||
|
||||
resolver(null, response);
|
||||
});
|
||||
|
||||
const notification = require('../src/notify-request');
|
||||
const infoArgs = [{ content }, 'A notification has been shipped: @{content}'];
|
||||
|
||||
await expect(notification.notifyRequest(options, content)).rejects.toThrow('body is missing');
|
||||
expect(logger.logger.info).toHaveBeenCalledWith(...infoArgs);
|
||||
});
|
||||
});
|
@ -20,7 +20,6 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build",
|
||||
"build": "tsc --emitDeclarationOnly -p tsconfig.build.json",
|
||||
"test": "echo \"Error: no test specified\" && exit 0"
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import distTagsMerge from './tags/dist-tags-merge';
|
||||
import addtag from './tags/addtag';
|
||||
import adduser from './adduser/adduser';
|
||||
import logout from './adduser/logout';
|
||||
import notify from './notifications/notify';
|
||||
import incomplete from './sanity/incomplete';
|
||||
import mirror from './sanity/mirror';
|
||||
import readme from './readme/readme';
|
||||
@ -56,7 +55,6 @@ describe('functional test verdaccio', function () {
|
||||
security(server1);
|
||||
addtag(server1);
|
||||
pluginsAuth(server2);
|
||||
notify(app);
|
||||
uplinkTimeout(server1, server2, server3);
|
||||
// requires packages published to server1/server2
|
||||
upLinkCache(server1, server2, server3);
|
||||
|
@ -1,175 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { HEADERS } from '@verdaccio/dev-commons';
|
||||
import { notify } from '@verdaccio/hooks';
|
||||
import { DOMAIN_SERVERS, PORT_SERVER_APP } from '../config.functional';
|
||||
import { RemoteUser } from '@verdaccio/types';
|
||||
|
||||
export default function (express) {
|
||||
const config = {
|
||||
notify: {
|
||||
method: 'POST',
|
||||
headers: [
|
||||
{
|
||||
'Content-Type': HEADERS.JSON,
|
||||
},
|
||||
],
|
||||
endpoint: `http://${DOMAIN_SERVERS}:${PORT_SERVER_APP}/api/notify`,
|
||||
content: `{"color":"green","message":"New package published: * {{ name }}*. Publisher name: * {{ publisher.name }} *.","notify":true,"message_format":"text"}`,
|
||||
},
|
||||
};
|
||||
|
||||
const publisherInfo: RemoteUser = {
|
||||
name: 'publisher-name-test',
|
||||
real_groups: [],
|
||||
groups: [],
|
||||
};
|
||||
|
||||
describe('notifications', () => {
|
||||
function parseBody(notification) {
|
||||
const jsonBody = JSON.parse(notification);
|
||||
|
||||
return jsonBody;
|
||||
}
|
||||
|
||||
beforeAll(function () {
|
||||
express.post('/api/notify', function (req, res) {
|
||||
res.send(req.body);
|
||||
});
|
||||
express.post('/api/notify/bad', function (req, res) {
|
||||
res.status(400);
|
||||
res.send('bad response');
|
||||
});
|
||||
});
|
||||
|
||||
test('notification should be send', (done) => {
|
||||
const metadata = {
|
||||
name: 'pkg-test',
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
notify(metadata, config, publisherInfo, 'foo').then(
|
||||
function (body) {
|
||||
const jsonBody = parseBody(body);
|
||||
expect(
|
||||
`New package published: * ${metadata.name}*. Publisher name: * ${publisherInfo.name} *.`
|
||||
).toBe(jsonBody.message);
|
||||
done();
|
||||
},
|
||||
function (err) {
|
||||
expect(err).toBeDefined();
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('notification should be send single header', (done) => {
|
||||
const metadata = {
|
||||
name: 'pkg-test',
|
||||
};
|
||||
|
||||
const configMultipleHeader = _.cloneDeep(config);
|
||||
configMultipleHeader.notify.headers = {
|
||||
// @ts-ignore
|
||||
'Content-Type': HEADERS.JSON,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
notify(metadata, configMultipleHeader, publisherInfo).then(
|
||||
function (body) {
|
||||
const jsonBody = parseBody(body);
|
||||
expect(
|
||||
`New package published: * ${metadata.name}*. Publisher name: * ${publisherInfo.name} *.`
|
||||
).toBe(jsonBody.message);
|
||||
done();
|
||||
},
|
||||
function (err) {
|
||||
expect(err).toBeDefined();
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('notification should be send multiple notifications endpoints', (done) => {
|
||||
const metadata = {
|
||||
name: 'pkg-test',
|
||||
};
|
||||
// let notificationsCounter = 0;
|
||||
|
||||
const multipleNotificationsEndpoint = {
|
||||
notify: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const notificationSettings = _.cloneDeep(config.notify);
|
||||
// basically we allow al notifications
|
||||
// @ts-ignore
|
||||
notificationSettings.packagePattern = /^pkg-test$/;
|
||||
// notificationSettings.packagePatternFlags = 'i';
|
||||
// @ts-ignore
|
||||
multipleNotificationsEndpoint.notify.push(notificationSettings);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
notify(metadata, multipleNotificationsEndpoint, publisherInfo).then(
|
||||
function (body) {
|
||||
body.forEach(function (notification) {
|
||||
const jsonBody = parseBody(notification);
|
||||
expect(
|
||||
`New package published: * ${metadata.name}*. Publisher name: * ${publisherInfo.name} *.`
|
||||
).toBe(jsonBody.message);
|
||||
});
|
||||
done();
|
||||
},
|
||||
function (err) {
|
||||
expect(err).toBeDefined();
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('notification should fails', (done) => {
|
||||
const metadata = {
|
||||
name: 'pkg-test',
|
||||
};
|
||||
const configFail = _.cloneDeep(config);
|
||||
configFail.notify.endpoint = `http://${DOMAIN_SERVERS}:${PORT_SERVER_APP}/api/notify/bad`;
|
||||
|
||||
// @ts-ignore
|
||||
notify(metadata, configFail, publisherInfo).then(
|
||||
function () {
|
||||
expect(false).toBe('This service should fails with status code 400');
|
||||
done();
|
||||
},
|
||||
function (err) {
|
||||
expect(err).toEqual('bad response');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('publisher property should not be overridden if it exists in metadata', (done) => {
|
||||
const metadata = {
|
||||
name: 'pkg-test',
|
||||
publisher: {
|
||||
name: 'existing-publisher-name',
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
notify(metadata, config, publisherInfo).then(
|
||||
function (body) {
|
||||
const jsonBody = parseBody(body);
|
||||
expect(
|
||||
`New package published: * ${metadata.name}*. Publisher name: * ${metadata.publisher.name} *.`
|
||||
).toBe(jsonBody.message);
|
||||
done();
|
||||
},
|
||||
function (err) {
|
||||
expect(err).toBeDefined();
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
54
pnpm-lock.yaml
generated
54
pnpm-lock.yaml
generated
@ -216,6 +216,7 @@ importers:
|
||||
supertest: next
|
||||
packages/auth:
|
||||
dependencies:
|
||||
'@verdaccio/auth': 'link:'
|
||||
'@verdaccio/commons-api': 'link:../core/commons-api'
|
||||
'@verdaccio/dev-commons': 'link:../commons'
|
||||
'@verdaccio/loaders': 'link:../loaders'
|
||||
@ -229,6 +230,7 @@ importers:
|
||||
'@verdaccio/mock': 'link:../mock'
|
||||
'@verdaccio/types': 'link:../core/types'
|
||||
specifiers:
|
||||
'@verdaccio/auth': 'workspace:5.0.0-alpha.0'
|
||||
'@verdaccio/commons-api': 'workspace:*'
|
||||
'@verdaccio/config': 'workspace:5.0.0-alpha.0'
|
||||
'@verdaccio/dev-commons': 'workspace:5.0.0-alpha.0'
|
||||
@ -265,7 +267,7 @@ importers:
|
||||
devDependencies:
|
||||
'@verdaccio/types': 'link:../core/types'
|
||||
specifiers:
|
||||
'@verdaccio/types': 'workspace:10.0.0-beta'
|
||||
'@verdaccio/types': 'workspace:*'
|
||||
packages/config:
|
||||
dependencies:
|
||||
'@verdaccio/dev-commons': 'link:../commons'
|
||||
@ -354,9 +356,9 @@ importers:
|
||||
marked: 1.1.1
|
||||
packages/core/streams:
|
||||
devDependencies:
|
||||
'@verdaccio/types': 9.7.2
|
||||
'@verdaccio/types': 'link:../types'
|
||||
specifiers:
|
||||
'@verdaccio/types': ^9.7.2
|
||||
'@verdaccio/types': 'workspace:*'
|
||||
packages/core/types:
|
||||
devDependencies:
|
||||
'@types/node': 14.6.0
|
||||
@ -366,8 +368,9 @@ importers:
|
||||
dependencies:
|
||||
'@verdaccio/commons-api': 'link:../core/commons-api'
|
||||
'@verdaccio/logger': 'link:../logger'
|
||||
debug: 4.2.0
|
||||
handlebars: 4.5.3
|
||||
lodash: 4.17.20
|
||||
node-fetch: 2.6.1
|
||||
request: 2.87.0
|
||||
devDependencies:
|
||||
'@verdaccio/auth': 'link:../auth'
|
||||
@ -375,6 +378,7 @@ importers:
|
||||
'@verdaccio/dev-commons': 'link:../commons'
|
||||
'@verdaccio/types': 'link:../core/types'
|
||||
'@verdaccio/utils': 'link:../utils'
|
||||
nock: 13.0.4
|
||||
specifiers:
|
||||
'@verdaccio/auth': 'workspace:5.0.0-alpha.0'
|
||||
'@verdaccio/commons-api': 'workspace:*'
|
||||
@ -383,8 +387,10 @@ importers:
|
||||
'@verdaccio/logger': 'workspace:5.0.0-alpha.0'
|
||||
'@verdaccio/types': 'workspace:*'
|
||||
'@verdaccio/utils': 'workspace:5.0.0-alpha.0'
|
||||
debug: ^4.2.0
|
||||
handlebars: 4.5.3
|
||||
lodash: ^4.17.20
|
||||
nock: ^13.0.4
|
||||
node-fetch: ^2.6.1
|
||||
request: 2.87.0
|
||||
packages/loaders:
|
||||
dependencies:
|
||||
@ -5239,10 +5245,6 @@ packages:
|
||||
npm: '>=5'
|
||||
resolution:
|
||||
integrity: sha512-SoCG1btVFPxOcrs8w9wLJCfe8nfE6EaEXCXyRwGbh+Sr3NLEG0R8JOugGJbuSE+zIRuUs5JaUKjzSec+JKLvZw==
|
||||
/@verdaccio/types/9.7.2:
|
||||
dev: true
|
||||
resolution:
|
||||
integrity: sha512-zv8sMrghtrzkxfo+IOojZYk/j4D5kmF/DMwXS9GnmObTM2nAOTsBzlOxHHBdHaiOK+cntw2YRYQJAebMG5J5sA==
|
||||
/@verdaccio/ui-theme/0.3.13:
|
||||
dev: true
|
||||
engines:
|
||||
@ -8406,6 +8408,19 @@ packages:
|
||||
ms: 2.1.2
|
||||
resolution:
|
||||
integrity: sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
|
||||
/debug/4.2.0:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
dev: false
|
||||
engines:
|
||||
node: '>=6.0'
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
resolution:
|
||||
integrity: sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
|
||||
/decamelize-keys/1.1.0:
|
||||
dependencies:
|
||||
decamelize: 1.2.0
|
||||
@ -14925,6 +14940,10 @@ packages:
|
||||
dev: false
|
||||
resolution:
|
||||
integrity: sha1-XkKRsMdT+hq+sKq4+ynfG2bwf20=
|
||||
/lodash.set/4.3.2:
|
||||
dev: true
|
||||
resolution:
|
||||
integrity: sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
|
||||
/lodash.sortby/4.7.0:
|
||||
resolution:
|
||||
integrity: sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||
@ -15875,6 +15894,17 @@ packages:
|
||||
node: '>= 10.13'
|
||||
resolution:
|
||||
integrity: sha512-QNb/j8kbFnKCiyqi9C5DD0jH/FubFGj5rt9NQFONXwQm3IPB0CULECg/eS3AU1KgZb/6SwUa4/DTRKhVxkGABw==
|
||||
/nock/13.0.4:
|
||||
dependencies:
|
||||
debug: 4.1.1
|
||||
json-stringify-safe: 5.0.1
|
||||
lodash.set: 4.3.2
|
||||
propagate: 2.0.1
|
||||
dev: true
|
||||
engines:
|
||||
node: '>= 10.13'
|
||||
resolution:
|
||||
integrity: sha512-alqTV8Qt7TUbc74x1pKRLSENzfjp4nywovcJgi/1aXDiUxXdt7TkruSTF5MDWPP7UoPVgea4F9ghVdmX0xxnSA==
|
||||
/node-abi/2.19.1:
|
||||
dependencies:
|
||||
semver: 5.7.1
|
||||
@ -15920,6 +15950,12 @@ packages:
|
||||
node: 4.x || >=6.0.0
|
||||
resolution:
|
||||
integrity: sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
||||
/node-fetch/2.6.1:
|
||||
dev: false
|
||||
engines:
|
||||
node: 4.x || >=6.0.0
|
||||
resolution:
|
||||
integrity: sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
/node-forge/0.9.0:
|
||||
engines:
|
||||
node: '>= 4.5.0'
|
||||
|
Loading…
Reference in New Issue
Block a user