Ghost/ghost/mail-events/test/MailEventService.test.ts
Jono M 5e057dee11
Added tests to AdminX framework package (#19022)
refs https://github.com/TryGhost/Product/issues/4159

---

<!-- Leave the line below if you'd like GitHub Copilot to generate a
summary from your commit -->
<!--
copilot:summary
-->
### <samp>🤖[[deprecated]](https://githubnext.com/copilot-for-prs-sunset)
Generated by Copilot at 9e68f4d</samp>

This pull request refactors several components in the `admin-x-settings`
app to use common hooks from the `@tryghost/admin-x-framework` package,
which reduces code duplication and improves consistency. It also updates
the `package.json` file and adds unit tests for the `admin-x-framework`
package, which improves the formatting, testing, and dependency
management. Additionally, it makes some minor changes to the `hooks.ts`,
`FrameworkProvider.tsx`, and `.eslintrc.cjs` files in the
`admin-x-framework` package, which enhance the public API and the
linting configuration.
2023-11-20 11:00:51 +00:00

367 lines
12 KiB
TypeScript

import assert from 'assert/strict';
import sinon from 'sinon';
import {InMemoryMailEventRepository as MailEventRepository} from '../src/InMemoryMailEventRepository';
import {MailEvent} from '../src/MailEvent';
import {MailEventService} from '../src/MailEventService';
const makePayloadEvent = (
type: string,
timestamp = Date.now()
) => ({
id: 'event-id',
timestamp: timestamp / 1000,
event: type,
message: {
headers: {
'message-id': 'message-id'
}
},
recipient: 'message-recipient'
});
const PAYLOAD_SIGNING_KEY = 'abc123';
describe('MailEventService', function () {
let repository: sinon.SinonStubbedInstance<MailEventRepository>;
let config: sinon.SinonStubbedInstance<any>;
let labs: sinon.SinonStubbedInstance<any>;
let service: MailEventService;
beforeEach(function () {
repository = sinon.createStubInstance(MailEventRepository);
labs = {
isSet: sinon.stub()
.withArgs(MailEventService.LABS_KEY)
.returns(true)
};
config = {
get: sinon.stub()
.withArgs(MailEventService.CONFIG_KEY_PAYLOAD_SIGNING_KEY)
.returns(PAYLOAD_SIGNING_KEY)
};
service = new MailEventService(repository, config, labs);
});
describe('processPayload', function () {
it('should reject if labs flag is false', async function () {
labs.isSet.withArgs(MailEventService.LABS_KEY).returns(false);
await assert.rejects(
service.processPayload({} as any),
{
name: 'NotFoundError',
message: 'Resource could not be found.'
}
);
});
it('should reject if payload signing key is invalid', async function () {
config.get.withArgs(MailEventService.CONFIG_KEY_PAYLOAD_SIGNING_KEY).returns(undefined);
await assert.rejects(
service.processPayload({} as any),
{
name: 'InternalServerError',
message: 'payload signing key is not configured'
}
);
});
it('should reject if payload verification fails', async function () {
await assert.rejects(
service.processPayload({
signature: 'foobarbaz',
mail_events: [
makePayloadEvent('opened')
]
} as any),
{
name: 'UnauthorizedError',
message: '"signature" is invalid'
}
);
});
it('should store a single event', async function () {
const payloadEvent = makePayloadEvent('opened');
// Ensure a fixed timestamp is used so that we know the signature up front
payloadEvent.timestamp = 1686665992511 / 1000;
const payload = {
signature: '9f2567330688b82759600fad93c93b3e8f571d397c33688a8620400af20b79b3',
mail_events: [
payloadEvent
]
};
await service.processPayload(payload);
const storedEvent = repository.save.getCall(0).args[0];
assert.ok(storedEvent instanceof MailEvent);
assert.equal(storedEvent.id, payloadEvent.id);
});
it('should store multiple events', async function () {
const events = [
makePayloadEvent('opened'),
makePayloadEvent('opened')
];
// Ensure fixed timestamps are used so that we know the signature up front
events[0].timestamp = 1686665992511 / 1000;
events[1].timestamp = 1686665992512 / 1000;
const payload = {
signature: '02959cc9731ee575b66969a508f545d19c5968b42a03fa398ce9c93d8e7df0a5',
mail_events: events
};
await service.processPayload(payload);
assert.ok(repository.save.calledTwice);
});
it('should ignore unknown events', async function () {
const events = [
makePayloadEvent('unknown-event'),
makePayloadEvent('opened')
];
// Ensure fixed timestamps are used so that we know the signature up front
events[0].timestamp = 1686665992511 / 1000;
events[1].timestamp = 1686665992512 / 1000;
const payload = {
signature: 'd6de350faa9ec56d739ec7ffd5cb4230f90f583df05fe59a6c1a41afac7048df',
mail_events: events
};
await service.processPayload(payload);
assert.ok(repository.save.calledOnce);
assert.equal(repository.save.getCall(0).args[0].type, 'opened');
});
it('should ensure event timestamps are converted to ms', async function () {
const payloadEvent = makePayloadEvent('opened');
// Ensure a fixed timestamp is used so that we know the signature up front
payloadEvent.timestamp = 1686665992511 / 1000;
const payload = {
signature: '9f2567330688b82759600fad93c93b3e8f571d397c33688a8620400af20b79b3',
mail_events: [
payloadEvent
]
};
await service.processPayload(payload);
assert.ok(repository.save.calledOnce);
const storedEvent = repository.save.getCall(0).args[0];
assert.equal(storedEvent.timestampMs, payloadEvent.timestamp * 1000);
});
it('should reject if an event can not be stored', async function () {
const payloadEvent = makePayloadEvent('opened');
// Ensure a fixed timestamp is used so that we know the signature up front
payloadEvent.timestamp = 1686665992511 / 1000;
const payload = {
signature: '9f2567330688b82759600fad93c93b3e8f571d397c33688a8620400af20b79b3',
mail_events: [
payloadEvent
]
};
repository.save.rejects(new Error('foobarbaz'));
await assert.rejects(
service.processPayload(payload),
{
name: 'InternalServerError',
message: 'Event could not be stored'
}
);
});
});
describe('validatePayload', function () {
it('should validate that the payload contains a signature', function () {
assert.throws(
() => service.validatePayload({} as any),
{
name: 'ValidationError',
message: 'Payload is missing "signature"'
}
);
});
it('should validate that the payload contains a valid signature', function () {
assert.throws(() => {
service.validatePayload({
signature: {}
} as any);
}, {
name: 'ValidationError',
message: '"signature" is invalid'
});
});
it('should validate that the payload contains events', function () {
assert.throws(() => {
service.validatePayload({
signature: 'foobarbaz'
} as any);
}, {
name: 'ValidationError',
message: 'Payload is missing "mail_events"'
});
});
it('should validate that the payload contains valid events', function () {
assert.throws(() => {
service.validatePayload({
signature: 'foobarbaz',
mail_events: {}
} as any);
}, {
name: 'ValidationError',
message: '"mail_events" is not an array'
});
});
it('should validate that events in the payload have an id', function () {
const malformedPayloadEvent = makePayloadEvent('opened') as any;
delete malformedPayloadEvent.id;
const payload = {
signature: 'foobarbaz',
mail_events: [
makePayloadEvent('opened'),
malformedPayloadEvent
]
};
assert.throws(
() => service.validatePayload(payload),
{
name: 'ValidationError',
message: 'Event [1] is missing "id"'
}
);
});
it('should validate that events in the payload have an timestamp', function () {
const malformedPayloadEvent = makePayloadEvent('opened') as any;
delete malformedPayloadEvent.timestamp;
const payload = {
signature: 'foobarbaz',
mail_events: [
makePayloadEvent('opened'),
malformedPayloadEvent
]
};
assert.throws(
() => service.validatePayload(payload),
{
name: 'ValidationError',
message: 'Event [1] is missing "timestamp"'
}
);
});
it('should validate that events in the payload have an event', function () {
const malformedPayloadEvent = makePayloadEvent('opened') as any;
delete malformedPayloadEvent.event;
const payload = {
signature: 'foobarbaz',
mail_events: [
makePayloadEvent('opened'),
malformedPayloadEvent
]
};
assert.throws(
() => service.validatePayload(payload),
{
name: 'ValidationError',
message: 'Event [1] is missing "event"'
}
);
});
it('should validate that events in the payload have a message', function () {
const malformedPayloadEvent = makePayloadEvent('opened') as any;
delete malformedPayloadEvent.message;
const payload = {
signature: 'foobarbaz',
mail_events: [
makePayloadEvent('opened'),
malformedPayloadEvent
]
};
assert.throws(
() => service.validatePayload(payload),
{
name: 'ValidationError',
message: 'Event [1] is missing "message"'
}
);
});
it('should validate that events in the payload have a recipient', function () {
const malformedPayloadEvent = makePayloadEvent('opened') as any;
delete malformedPayloadEvent.recipient;
const payload = {
signature: 'foobarbaz',
mail_events: [
makePayloadEvent('opened'),
malformedPayloadEvent
]
};
assert.throws(
() => service.validatePayload(payload),
{
name: 'ValidationError',
message: 'Event [1] is missing "recipient"'
}
);
});
it('should validate that "message.headers.message-id" is present on an event', function () {
const malformedPayloadEvent = makePayloadEvent('opened') as any;
delete malformedPayloadEvent.message.headers;
const payload = {
signature: 'foobarbaz',
mail_events: [
makePayloadEvent('opened'),
malformedPayloadEvent
]
};
assert.throws(
() => service.validatePayload(payload),
{
name: 'ValidationError',
message: 'Event [1] is missing "message.headers.message-id"'
}
);
});
});
});