@eventcatalog/notifier
Version:
CLI tool to detect EventCatalog changes and send notifications
427 lines (393 loc) • 12.2 kB
text/typescript
import { NotifierConfig, Notification } from '../src/types';
import { sendNotifications } from '../src/notifier';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as yaml from 'js-yaml';
import axios from 'axios';
vi.mock('axios');
const mockedAxios = vi.mocked(axios);
const mockedAxiosPost = vi.mocked(axios.post);
describe('Notifier', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('sends slack notifications to configured owners for matching events (actual sending)', async () => {
mockedAxiosPost.mockResolvedValue({ status: 200 } as any);
const notifications = [
{
id: 'consumer-added',
resource: {
id: 'PaymentComplete',
name: 'Payment Complete',
version: '0.0.2',
type: 'event',
owners: [
{
id: 'dboyne',
},
],
},
consumer: {
id: 'PaymentService',
name: 'Payment Service',
version: '0.0.1',
type: 'service',
owners: [
{
id: 'dboyne',
},
],
},
metadata: {
timestamp: '2025-07-23T09:49:01.605Z',
catalog_path: '/Users/dboyne/dev/eventcatalog/eventcatalog-notifier/catalog-example',
},
},
] as Notification[];
const CONFIGURATION = `
version: 1.0.0
eventcatalog_url: https://eventcatalog.example.com
owners:
dboyne:
events:
- consumer-added
channels:
- type: slack
webhook: https://fake-slack-webhook.com
payments-team:
events:
- consumer-added
channels:
- type: slack
webhook: https://fake-slack-webhook.com
`;
const config = yaml.load(CONFIGURATION) as NotifierConfig;
await sendNotifications(config, notifications, { dryRun: false, lifecycle: 'active', actionUrl: 'https://example.com' });
expect(mockedAxiosPost).toHaveBeenCalledTimes(2);
expect(mockedAxiosPost).toHaveBeenCalledWith(
'https://fake-slack-webhook.com',
expect.objectContaining({
text: '🆕 EventCatalog - ✉️ New Event Consumer Added',
attachments: expect.arrayContaining([
expect.objectContaining({
color: 'good',
pretext: 'A new service has started consuming an event in your architecture',
fields: expect.arrayContaining([
expect.objectContaining({
title: '📡 Event Being Consumed',
value: expect.stringContaining('Payment Complete'),
}),
]),
}),
]),
}),
expect.objectContaining({
headers: {},
})
);
// Notification sent successfully - no need to test console output
});
it('logs notifications in dry run mode without sending', async () => {
const notifications = [
{
id: 'consumer-added',
resource: {
id: 'PaymentComplete',
name: 'Payment Complete',
version: '0.0.2',
type: 'event',
owners: [
{
id: 'dboyne',
},
],
},
consumer: {
id: 'PaymentService',
name: 'Payment Service',
version: '0.0.1',
type: 'service',
owners: [
{
id: 'dboyne',
},
],
},
metadata: {
timestamp: '2025-07-23T09:49:01.605Z',
catalog_path: '/Users/dboyne/dev/eventcatalog/eventcatalog-notifier/catalog-example',
},
},
] as Notification[];
const CONFIGURATION = `
version: 1.0.0
eventcatalog_url: https://eventcatalog.example.com
owners:
payments-team:
events:
- consumer-added
channels:
- type: slack
webhook: https://fake-slack-webhook.com
`;
const config = yaml.load(CONFIGURATION) as NotifierConfig;
await sendNotifications(config, notifications, { dryRun: true, lifecycle: 'active', actionUrl: 'https://example.com' });
expect(mockedAxiosPost).not.toHaveBeenCalled();
});
it('does not send notifications for events not configured for any owner', async () => {
const notifications = [
{
id: 'order-created', // Different event type
resource: {
id: 'OrderCreated',
name: 'Order Created',
version: '0.0.1',
type: 'event',
owners: [
{
id: 'dboyne',
},
],
},
consumer: {
id: 'OrderService',
name: 'Order Service',
version: '0.0.1',
type: 'service',
owners: [
{
id: 'dboyne',
},
],
},
metadata: {
timestamp: '2025-07-23T09:49:01.605Z',
catalog_path: '/Users/dboyne/dev/eventcatalog/eventcatalog-notifier/catalog-example',
},
},
] as Notification[];
const CONFIGURATION = `
version: 1.0.0
eventcatalog_url: https://eventcatalog.example.com
owners:
payments-team:
events:
- consumer-added # Only interested in consumer-added events
channels:
- type: slack
webhook: https://fake-slack-webhook.com
`;
const config = yaml.load(CONFIGURATION) as NotifierConfig;
await sendNotifications(config, notifications, { dryRun: true, lifecycle: 'active', actionUrl: 'https://example.com' });
expect(mockedAxiosPost).not.toHaveBeenCalled();
});
it('sends notifications to multiple channels for the same owner', async () => {
const notifications = [
{
id: 'consumer-added',
resource: {
id: 'PaymentComplete',
name: 'Payment Complete',
version: '0.0.2',
type: 'event',
owners: [
{
id: 'dboyne',
},
],
},
consumer: {
id: 'PaymentService',
name: 'Payment Service',
version: '0.0.1',
type: 'service',
owners: [
{
id: 'dboyne',
},
],
},
metadata: {
timestamp: '2025-07-23T09:49:01.605Z',
catalog_path: '/Users/dboyne/dev/eventcatalog/eventcatalog-notifier/catalog-example',
},
},
] as Notification[];
const CONFIGURATION = `
version: 1.0.0
eventcatalog_url: https://eventcatalog.example.com
owners:
payments-team:
events:
- consumer-added
channels:
- type: slack
webhook: https://fake-slack-webhook-1.com
- type: slack
webhook: https://fake-slack-webhook-2.com
`;
const config = yaml.load(CONFIGURATION) as NotifierConfig;
await sendNotifications(config, notifications, { dryRun: true, lifecycle: 'active', actionUrl: 'https://example.com' });
expect(mockedAxiosPost).not.toHaveBeenCalled();
});
it('handles HTTP errors when sending notifications', async () => {
const error = new Error('Network error');
mockedAxiosPost.mockRejectedValue(error);
const notifications = [
{
id: 'consumer-added',
resource: {
id: 'PaymentComplete',
name: 'Payment Complete',
version: '0.0.2',
type: 'event',
owners: [
{
id: 'dboyne',
},
],
},
consumer: {
id: 'PaymentService',
name: 'Payment Service',
version: '0.0.1',
type: 'service',
owners: [
{
id: 'dboyne',
},
],
},
metadata: {
timestamp: '2025-07-23T09:49:01.605Z',
catalog_path: '/Users/dboyne/dev/eventcatalog/eventcatalog-notifier/catalog-example',
},
},
] as Notification[];
const CONFIGURATION = `
version: 1.0.0
eventcatalog_url: https://eventcatalog.example.com
owners:
dboyne:
events:
- consumer-added
channels:
- type: slack
webhook: https://fake-slack-webhook.com
`;
const config = yaml.load(CONFIGURATION) as NotifierConfig;
await expect(
sendNotifications(config, notifications, { dryRun: false, lifecycle: 'active', actionUrl: 'https://example.com' })
).rejects.toThrow('Network error');
});
it('sends notifications with custom headers when specified', async () => {
mockedAxiosPost.mockResolvedValue({ status: 200 } as any);
const notifications = [
{
id: 'consumer-added',
resource: {
id: 'PaymentComplete',
name: 'Payment Complete',
version: '0.0.2',
type: 'event',
owners: [
{
id: 'dboyne',
},
],
},
consumer: {
id: 'PaymentService',
name: 'Payment Service',
version: '0.0.1',
type: 'service',
owners: [
{
id: 'dboyne',
},
],
},
metadata: {
timestamp: '2025-07-23T09:49:01.605Z',
catalog_path: '/Users/dboyne/dev/eventcatalog/eventcatalog-notifier/catalog-example',
},
},
] as Notification[];
const CONFIGURATION = `
version: 1.0.0
eventcatalog_url: https://eventcatalog.example.com
owners:
dboyne:
events:
- consumer-added
channels:
- type: slack
webhook: https://fake-slack-webhook.com
headers:
Authorization: Bearer secret-token
X-Custom-Header: custom-value
`;
const config = yaml.load(CONFIGURATION) as NotifierConfig;
await sendNotifications(config, notifications, { dryRun: false, lifecycle: 'active', actionUrl: 'https://example.com' });
expect(mockedAxiosPost).toHaveBeenCalledWith(
'https://fake-slack-webhook.com',
expect.objectContaining({
text: '🆕 EventCatalog - ✉️ New Event Consumer Added',
}),
expect.objectContaining({
headers: {
Authorization: 'Bearer secret-token',
'X-Custom-Header': 'custom-value',
},
})
);
});
it('logs headers in dry run mode when custom headers are specified', async () => {
const notifications = [
{
id: 'consumer-added',
resource: {
id: 'PaymentComplete',
name: 'Payment Complete',
version: '0.0.2',
type: 'event',
owners: [
{
id: 'dboyne',
},
],
},
consumer: {
id: 'PaymentService',
name: 'Payment Service',
version: '0.0.1',
type: 'service',
owners: [
{
id: 'dboyne',
},
],
},
metadata: {
timestamp: '2025-07-23T09:49:01.605Z',
catalog_path: '/Users/dboyne/dev/eventcatalog/eventcatalog-notifier/catalog-example',
},
},
] as Notification[];
const CONFIGURATION = `
version: 1.0.0
eventcatalog_url: https://eventcatalog.example.com
owners:
payments-team:
events:
- consumer-added
channels:
- type: slack
webhook: https://fake-slack-webhook.com
headers:
Authorization: Bearer secret-token
`;
const config = yaml.load(CONFIGURATION) as NotifierConfig;
await sendNotifications(config, notifications, { dryRun: true, lifecycle: 'active', actionUrl: 'https://example.com' });
expect(mockedAxiosPost).not.toHaveBeenCalled();
});
});