From 65cb26cf319c8a1ba64f22414e4433ecd427520d Mon Sep 17 00:00:00 2001 From: Juan Picado Date: Tue, 22 Sep 2020 23:43:39 +0200 Subject: [PATCH] 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 --- .babelrc | 4 + .changeset/late-parents-act.md | 6 + Dockerfile | 6 +- packages/auth/package.json | 1 + packages/commons/package.json | 2 +- packages/core/streams/package.json | 2 +- packages/core/types/index.d.ts | 5 + packages/hooks/package.json | 8 +- packages/hooks/src/notify-request.ts | 49 +++-- packages/hooks/src/notify.ts | 133 ++++++++----- packages/hooks/test/notify-request.spec.ts | 85 +++++++++ packages/hooks/test/notify.spec.ts | 87 --------- .../config/yaml/notify/multiple.notify.yaml | 6 +- .../yaml/notify/single.header.notify.yaml | 2 +- packages/hooks/test/request.spec.ts | 106 ----------- packages/types/package.json | 1 - .../verdaccio/test/functional/index.spec.ts | 2 - .../test/functional/notifications/notify.ts | 175 ------------------ pnpm-lock.yaml | 54 +++++- 19 files changed, 278 insertions(+), 456 deletions(-) create mode 100644 .changeset/late-parents-act.md create mode 100644 packages/hooks/test/notify-request.spec.ts delete mode 100644 packages/hooks/test/notify.spec.ts delete mode 100644 packages/hooks/test/request.spec.ts delete mode 100644 packages/verdaccio/test/functional/notifications/notify.ts diff --git a/.babelrc b/.babelrc index 868559f91..7fd839554 100644 --- a/.babelrc +++ b/.babelrc @@ -2,6 +2,10 @@ "presets": [ [ "@babel/env", { + "useBuiltIns": "usage", + "corejs": { + "version": 3, "proposals": true + }, "targets": { "node": 10 } diff --git a/.changeset/late-parents-act.md b/.changeset/late-parents-act.md new file mode 100644 index 000000000..a1c61a9eb --- /dev/null +++ b/.changeset/late-parents-act.md @@ -0,0 +1,6 @@ +--- +'@verdaccio/hooks': patch +'@verdaccio/proxy': patch +--- + +refactor: migrate request to node-fetch at hooks package diff --git a/Dockerfile b/Dockerfile index 8a57e9d42..287e8eaab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/packages/auth/package.json b/packages/auth/package.json index 5bf2d8e2e..bd3eaecb6 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -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" diff --git a/packages/commons/package.json b/packages/commons/package.json index 41b4cde0a..901576dce 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -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", diff --git a/packages/core/streams/package.json b/packages/core/streams/package.json index b28d40c7e..daa9346fa 100644 --- a/packages/core/streams/package.json +++ b/packages/core/streams/package.json @@ -23,7 +23,7 @@ "access": "public" }, "devDependencies": { - "@verdaccio/types": "^9.7.2" + "@verdaccio/types": "workspace:*" }, "scripts": { "clean": "rimraf ./build", diff --git a/packages/core/types/index.d.ts b/packages/core/types/index.d.ts index 81f234f7d..3f6bbb25e 100644 --- a/packages/core/types/index.d.ts +++ b/packages/core/types/index.d.ts @@ -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; diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 3212bbb5b..273d636a5 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -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", diff --git a/packages/hooks/src/notify-request.ts b/packages/hooks/src/notify-request.ts index 1b8f37d7c..bddcd2a1d 100644 --- a/packages/hooks/src/notify-request.ts +++ b/packages/hooks/src/notify-request.ts @@ -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 { - 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 { + 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; + } } diff --git a/packages/hooks/src/notify.ts b/packages/hooks/src/notify.ts index df8c7c955..98eab9e1b 100644 --- a/packages/hooks/src/notify.ts +++ b/packages/hooks/src/notify.ts @@ -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, notifyEntry, - remoteUser: RemoteUser, + remoteUser: Partial, publishedPackage: string -): Promise | void { +): Promise { 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, notify: Notification, - remoteUser: RemoteUser, + remoteUser: Partial, publishedPackage: string -): Promise { +): Promise { return handleNotify(metadata, notify, remoteUser, publishedPackage) as Promise; } -export function notify( - metadata: Package, - config: Config, - remoteUser: RemoteUser, +export async function notify( + metadata: Partial, + config: Partial, + remoteUser: Partial, publishedPackage: string -): Promise | void { +): Promise { + 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]; + } } diff --git a/packages/hooks/test/notify-request.spec.ts b/packages/hooks/test/notify-request.spec.ts new file mode 100644 index 000000000..d3b0c4b2d --- /dev/null +++ b/packages/hooks/test/notify-request.spec.ts @@ -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 = { + // @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]); + }); +}); diff --git a/packages/hooks/test/notify.spec.ts b/packages/hooks/test/notify.spec.ts deleted file mode 100644 index f0cbe8494..000000000 --- a/packages/hooks/test/notify.spec.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/packages/hooks/test/partials/config/yaml/notify/multiple.notify.yaml b/packages/hooks/test/partials/config/yaml/notify/multiple.notify.yaml index e61f10a70..e44406846 100644 --- a/packages/hooks/test/partials/config/yaml/notify/multiple.notify.yaml +++ b/packages/hooks/test/partials/config/yaml/notify/multiple.notify.yaml @@ -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 }}"}]}]}}' diff --git a/packages/hooks/test/partials/config/yaml/notify/single.header.notify.yaml b/packages/hooks/test/partials/config/yaml/notify/single.header.notify.yaml index bbb42046b..9fd2fb5d9 100644 --- a/packages/hooks/test/partials/config/yaml/notify/single.header.notify.yaml +++ b/packages/hooks/test/partials/config/yaml/notify/single.header.notify.yaml @@ -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"}' diff --git a/packages/hooks/test/request.spec.ts b/packages/hooks/test/request.spec.ts deleted file mode 100644 index 54e3a9490..000000000 --- a/packages/hooks/test/request.spec.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/types/package.json b/packages/types/package.json index 7e3eb8a71..97575408a 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -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" } } diff --git a/packages/verdaccio/test/functional/index.spec.ts b/packages/verdaccio/test/functional/index.spec.ts index c181f8813..2548c6dd1 100644 --- a/packages/verdaccio/test/functional/index.spec.ts +++ b/packages/verdaccio/test/functional/index.spec.ts @@ -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); diff --git a/packages/verdaccio/test/functional/notifications/notify.ts b/packages/verdaccio/test/functional/notifications/notify.ts deleted file mode 100644 index 051dc2808..000000000 --- a/packages/verdaccio/test/functional/notifications/notify.ts +++ /dev/null @@ -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(); - } - ); - }); - }); -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c132da6e..b2b899545 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'