246 lines
8.8 KiB
TypeScript
246 lines
8.8 KiB
TypeScript
import { describe, it, expect, afterEach } from 'vitest';
|
|
import {
|
|
NormalizationError,
|
|
SLACK_PENDING_TEXT,
|
|
isNoteInputArray,
|
|
normalizeSlack,
|
|
} from '../config/normalizers.js';
|
|
import type { NoteInput } from '../types/domain.js';
|
|
|
|
const sourceMeta = (input: NoteInput): Record<string, unknown> => {
|
|
expect(input.sourceMeta).toBeDefined();
|
|
return input.sourceMeta as Record<string, unknown>;
|
|
};
|
|
|
|
const slackPayload = (
|
|
event: Record<string, unknown> = {},
|
|
envelope: Record<string, unknown> = {}
|
|
): Record<string, unknown> => ({
|
|
type: 'event_callback',
|
|
event_id: 'Ev08K1QR2X',
|
|
team_id: 'T0123',
|
|
event: {
|
|
type: 'reaction_added',
|
|
user: 'U_REACTOR',
|
|
reaction: 'pushpin',
|
|
item_user: 'U_AUTHOR',
|
|
item: { type: 'message', channel: 'C0123', ts: '1700000000.000100' },
|
|
event_ts: '1700000001.000200',
|
|
...event,
|
|
},
|
|
...envelope,
|
|
});
|
|
|
|
const item = (overrides: Record<string, unknown> = {}): Record<string, unknown> => ({
|
|
type: 'message',
|
|
channel: 'C0123',
|
|
ts: '1700000000.000100',
|
|
...overrides,
|
|
});
|
|
|
|
const savedReaction = process.env.SLACK_CAPTURE_REACTION;
|
|
|
|
const setCaptureReaction = (value: string | undefined): void => {
|
|
if (value === undefined) {
|
|
delete process.env.SLACK_CAPTURE_REACTION;
|
|
return;
|
|
}
|
|
process.env.SLACK_CAPTURE_REACTION = value;
|
|
};
|
|
|
|
afterEach(() => {
|
|
setCaptureReaction(savedReaction);
|
|
});
|
|
|
|
describe('normalizeSlack', () => {
|
|
it('should map a captured reaction to a single placeholder note', () => {
|
|
const result = normalizeSlack(slackPayload());
|
|
|
|
expect(result.externalId).toBe('Ev08K1QR2X');
|
|
expect(result.notes).toHaveLength(1);
|
|
expect(result.notes[0].id).toBe('slack_Ev08K1QR2X');
|
|
expect(result.notes[0].text).toBe(SLACK_PENDING_TEXT);
|
|
expect(result.notes[0].author).toBe('U_AUTHOR');
|
|
});
|
|
|
|
it('should export the placeholder text used for the pending message body', () => {
|
|
expect(SLACK_PENDING_TEXT).toBe('(pending Slack message text)');
|
|
});
|
|
|
|
it('should attach provenance plus the identifiers the enrichment hook needs', () => {
|
|
const meta = sourceMeta(normalizeSlack(slackPayload()).notes[0]);
|
|
|
|
expect(meta).toMatchObject({
|
|
provider: 'slack',
|
|
externalId: 'Ev08K1QR2X',
|
|
needsMessageText: true,
|
|
channelId: 'C0123',
|
|
messageTs: '1700000000.000100',
|
|
teamId: 'T0123',
|
|
reaction: 'pushpin',
|
|
reactedBy: 'U_REACTOR',
|
|
authorUserId: 'U_AUTHOR',
|
|
});
|
|
expect(typeof meta.receivedAt).toBe('string');
|
|
});
|
|
|
|
it('should mark needsMessageText as exactly true', () => {
|
|
const meta = sourceMeta(normalizeSlack(slackPayload()).notes[0]);
|
|
|
|
expect(meta.needsMessageText).toBe(true);
|
|
});
|
|
|
|
it('should record an ISO receivedAt timestamp in provenance', () => {
|
|
const receivedAt = String(sourceMeta(normalizeSlack(slackPayload()).notes[0]).receivedAt);
|
|
|
|
expect(new Date(receivedAt).toISOString()).toBe(receivedAt);
|
|
});
|
|
|
|
it('should fall back to the reacting user when item_user is absent', () => {
|
|
expect(normalizeSlack(slackPayload({ item_user: undefined })).notes[0].author).toBe(
|
|
'U_REACTOR'
|
|
);
|
|
expect(normalizeSlack(slackPayload({ item_user: '' })).notes[0].author).toBe('U_REACTOR');
|
|
});
|
|
|
|
it('should fall back to an unknown author when neither user is present', () => {
|
|
const result = normalizeSlack(slackPayload({ item_user: undefined, user: undefined }));
|
|
|
|
expect(result.notes[0].author).toBe('unknown');
|
|
expect(sourceMeta(result.notes[0]).reactedBy).toBeUndefined();
|
|
expect(sourceMeta(result.notes[0]).authorUserId).toBeUndefined();
|
|
});
|
|
|
|
it('should omit teamId when the envelope carries none', () => {
|
|
const payload = slackPayload({}, { team_id: undefined });
|
|
|
|
expect(sourceMeta(normalizeSlack(payload).notes[0]).teamId).toBeUndefined();
|
|
});
|
|
|
|
it('should return zero notes for a reaction other than the configured one', () => {
|
|
expect(normalizeSlack(slackPayload({ reaction: 'eyes' }))).toEqual({
|
|
externalId: 'Ev08K1QR2X',
|
|
notes: [],
|
|
});
|
|
});
|
|
|
|
it('should honour a custom capture reaction from the environment', () => {
|
|
setCaptureReaction('thumbsup');
|
|
|
|
expect(normalizeSlack(slackPayload({ reaction: 'thumbsup' })).notes).toHaveLength(1);
|
|
expect(normalizeSlack(slackPayload({ reaction: 'pushpin' })).notes).toHaveLength(0);
|
|
});
|
|
|
|
it('should read the capture reaction at call time rather than at module load', () => {
|
|
setCaptureReaction('eyes');
|
|
expect(normalizeSlack(slackPayload({ reaction: 'eyes' })).notes).toHaveLength(1);
|
|
|
|
setCaptureReaction('rocket');
|
|
expect(normalizeSlack(slackPayload({ reaction: 'eyes' })).notes).toHaveLength(0);
|
|
expect(normalizeSlack(slackPayload({ reaction: 'rocket' })).notes).toHaveLength(1);
|
|
});
|
|
|
|
it('should match a reaction carrying a skin-tone modifier', () => {
|
|
setCaptureReaction('thumbsup');
|
|
const result = normalizeSlack(slackPayload({ reaction: 'thumbsup::skin-tone-3' }));
|
|
|
|
expect(result.notes).toHaveLength(1);
|
|
expect(sourceMeta(result.notes[0]).reaction).toBe('thumbsup::skin-tone-3');
|
|
});
|
|
|
|
it('should fall back to pushpin when no capture reaction is configured', () => {
|
|
setCaptureReaction(undefined);
|
|
|
|
expect(normalizeSlack(slackPayload()).notes).toHaveLength(1);
|
|
expect(normalizeSlack(slackPayload({ reaction: 'pushpin::skin-tone-5' })).notes).toHaveLength(
|
|
1
|
|
);
|
|
});
|
|
|
|
it('should return zero notes when the reaction is missing or not a string', () => {
|
|
expect(normalizeSlack(slackPayload({ reaction: undefined })).notes).toHaveLength(0);
|
|
expect(normalizeSlack(slackPayload({ reaction: 7 })).notes).toHaveLength(0);
|
|
expect(normalizeSlack(slackPayload({ reaction: '' })).notes).toHaveLength(0);
|
|
});
|
|
|
|
it('should return zero notes for an event type we do not capture', () => {
|
|
expect(normalizeSlack(slackPayload({ type: 'message' }))).toEqual({
|
|
externalId: 'Ev08K1QR2X',
|
|
notes: [],
|
|
});
|
|
expect(normalizeSlack(slackPayload({ type: 'reaction_removed' })).notes).toHaveLength(0);
|
|
});
|
|
|
|
it('should return zero notes when the event is missing or not an object', () => {
|
|
expect(normalizeSlack({ type: 'event_callback', event_id: 'Ev1' })).toEqual({
|
|
externalId: 'Ev1',
|
|
notes: [],
|
|
});
|
|
expect(normalizeSlack({ type: 'event_callback', event_id: 'Ev1', event: 'nope' })).toEqual({
|
|
externalId: 'Ev1',
|
|
notes: [],
|
|
});
|
|
});
|
|
|
|
it('should return zero notes when the reacted item is not a message', () => {
|
|
expect(normalizeSlack(slackPayload({ item: item({ type: 'file' }) }))).toEqual({
|
|
externalId: 'Ev08K1QR2X',
|
|
notes: [],
|
|
});
|
|
});
|
|
|
|
it('should return zero notes when the item channel is missing', () => {
|
|
expect(normalizeSlack(slackPayload({ item: item({ channel: undefined }) })).notes).toHaveLength(
|
|
0
|
|
);
|
|
expect(normalizeSlack(slackPayload({ item: item({ channel: '' }) })).notes).toHaveLength(0);
|
|
});
|
|
|
|
it('should return zero notes when the item timestamp is missing', () => {
|
|
expect(normalizeSlack(slackPayload({ item: item({ ts: undefined }) })).notes).toHaveLength(0);
|
|
expect(normalizeSlack(slackPayload({ item: item({ ts: 1700000000 }) })).notes).toHaveLength(0);
|
|
});
|
|
|
|
it('should return zero notes when the item itself is missing', () => {
|
|
expect(normalizeSlack(slackPayload({ item: undefined })).notes).toHaveLength(0);
|
|
});
|
|
|
|
it('should throw when event_id is missing or empty', () => {
|
|
expect(() => normalizeSlack(slackPayload({}, { event_id: undefined }))).toThrow(
|
|
NormalizationError
|
|
);
|
|
expect(() => normalizeSlack(slackPayload({}, { event_id: '' }))).toThrow(/missing event_id/);
|
|
expect(() => normalizeSlack(slackPayload({}, { event_id: 42 }))).toThrow(NormalizationError);
|
|
});
|
|
|
|
it('should throw for an envelope type other than event_callback', () => {
|
|
expect(() => normalizeSlack(slackPayload({}, { type: 'url_verification' }))).toThrow(
|
|
NormalizationError
|
|
);
|
|
expect(() => normalizeSlack(slackPayload({}, { type: undefined }))).toThrow(
|
|
/not an event_callback/
|
|
);
|
|
});
|
|
|
|
it('should throw for a non-object payload', () => {
|
|
expect(() => normalizeSlack(null)).toThrow(/not an object/);
|
|
expect(() => normalizeSlack(undefined)).toThrow(NormalizationError);
|
|
expect(() => normalizeSlack('event_callback')).toThrow(NormalizationError);
|
|
expect(() => normalizeSlack([slackPayload()])).toThrow(/not an object/);
|
|
});
|
|
|
|
it('should truncate a generated note id to 64 characters', () => {
|
|
const longId = 'E'.repeat(400);
|
|
const result = normalizeSlack(slackPayload({}, { event_id: longId }));
|
|
|
|
expect(result.externalId).toBe(longId);
|
|
expect(result.notes[0].id).toHaveLength(64);
|
|
expect(result.notes[0].id).toBe(`slack_${longId}`.slice(0, 64));
|
|
expect(sourceMeta(result.notes[0]).externalId).toBe(longId);
|
|
});
|
|
|
|
it('should produce notes accepted by the note input guard', () => {
|
|
expect(isNoteInputArray(normalizeSlack(slackPayload()).notes)).toBe(true);
|
|
});
|
|
});
|