diff --git a/packages/core/src/event-stream/encoder.test.ts b/packages/core/src/event-stream/encoder.test.ts index f33fe6f..164c3fd 100644 --- a/packages/core/src/event-stream/encoder.test.ts +++ b/packages/core/src/event-stream/encoder.test.ts @@ -8,8 +8,40 @@ import { encodeEventStreamMessage, encodeEventStreamMessageComments, encodeEventStreamMessageData, + isEventStreamMessageComment, + isEventStreamMessageId, + isEventStreamMessageRetry, } from './encoder' +describe('predicates', () => { + it('accept valid values', () => { + expect(isEventStreamMessageId('')).toBe(true) + expect(isEventStreamMessageId('123')).toBe(true) + expect(isEventStreamMessageComment('hi')).toBe(true) + expect(isEventStreamMessageRetry(0)).toBe(true) + expect(isEventStreamMessageRetry(10000)).toBe(true) + }) + + it('reject strings containing line breaks', () => { + for (const lineBreak of ['\n', '\r', '\r\n']) { + expect(isEventStreamMessageId(`hi${lineBreak}`)).toBe(false) + expect(isEventStreamMessageComment(`hi${lineBreak}`)).toBe(false) + } + }) + + it('reject non-integer or negative retry values', () => { + for (const retry of [Number.NaN, -1, 1.5, Number.POSITIVE_INFINITY]) { + expect(isEventStreamMessageRetry(retry)).toBe(false) + } + }) + + it('reject wrong types', () => { + expect(isEventStreamMessageId(123)).toBe(false) + expect(isEventStreamMessageComment(undefined)).toBe(false) + expect(isEventStreamMessageRetry('3000')).toBe(false) + }) +}) + describe('assertions', () => { it('accept values without line breaks', () => { expect(() => assertEventStreamMessageId('123')).not.toThrow() diff --git a/packages/core/src/event-stream/encoder.ts b/packages/core/src/event-stream/encoder.ts index 12b778f..ffa7646 100644 --- a/packages/core/src/event-stream/encoder.ts +++ b/packages/core/src/event-stream/encoder.ts @@ -8,8 +8,20 @@ function containsEventStreamLineBreak(value: string): boolean { return EVENT_STREAM_LINE_ENDING_REGEX.test(value) } +export function isEventStreamMessageId(maybe: unknown): maybe is string { + return typeof maybe === 'string' && !containsEventStreamLineBreak(maybe) +} + +export function isEventStreamMessageRetry(maybe: unknown): maybe is number { + return Number.isInteger(maybe) && (maybe as number) >= 0 +} + +export function isEventStreamMessageComment(maybe: unknown): maybe is string { + return typeof maybe === 'string' && !containsEventStreamLineBreak(maybe) +} + export function assertEventStreamMessageId(id: string): void { - if (containsEventStreamLineBreak(id)) { + if (!isEventStreamMessageId(id)) { throw new EventStreamEncoderError('Event\'s id must not contain a carriage return or newline character') } } @@ -21,13 +33,13 @@ export function assertEventStreamMessageName(event: string): void { } export function assertEventStreamMessageRetry(retry: number): void { - if (!Number.isInteger(retry) || retry < 0) { + if (!isEventStreamMessageRetry(retry)) { throw new EventStreamEncoderError('Event\'s retry must be a integer and >= 0') } } export function assertEventStreamMessageComment(comment: string): void { - if (containsEventStreamLineBreak(comment)) { + if (!isEventStreamMessageComment(comment)) { throw new EventStreamEncoderError('Event\'s comment must not contain a carriage return or newline character') } } diff --git a/packages/peer/src/validators.test.ts b/packages/peer/src/validators.test.ts index a7bacc2..e64bfd3 100644 --- a/packages/peer/src/validators.test.ts +++ b/packages/peer/src/validators.test.ts @@ -132,7 +132,11 @@ describe('isPeerEventStreamMessage', () => { ['non-string id', { id: 42 }], ['non-string event', { event: true }], ['non-finite retry', { retry: Infinity }], + ['negative retry', { retry: -1 }], + ['fractional retry', { retry: 1.5 }], + ['id with line break', { id: 'a\nb' }], ['non-string-array comments', { comments: [1, 2] }], + ['comment with line break', { comments: ['ok', 'a\rb'] }], ])('rejects %s', (_, json) => expect(isPeerEventStreamMessage(msg(json))).toBe(false)) }) diff --git a/packages/peer/src/validators.ts b/packages/peer/src/validators.ts index 083c037..f915f24 100644 --- a/packages/peer/src/validators.ts +++ b/packages/peer/src/validators.ts @@ -1,5 +1,5 @@ import type { ClientPeerSendMessage, PeerCancelMessage, PeerEventStreamMessage, PeerMessage, PeerOctetStreamMessage, PeerRequestMessage, PeerResponseMessage, PeerStreamCancelMessage, ServerPeerSendMessage } from './types' -import { isStandardHeaders, isStandardMethod, isStandardStatus, isStandardUrl } from '@standard-server/core' +import { isEventStreamMessageComment, isEventStreamMessageId, isEventStreamMessageRetry, isStandardHeaders, isStandardMethod, isStandardStatus, isStandardUrl } from '@standard-server/core' import { isTypescriptObject } from '@standard-server/shared' export function isPeerMessage(maybe: unknown): maybe is PeerMessage { @@ -75,7 +75,7 @@ export function isPeerEventStreamMessage(maybe: PeerMessage): maybe is PeerEvent return false } - if (maybe.json.id !== undefined && typeof maybe.json.id !== 'string') { + if (maybe.json.id !== undefined && !isEventStreamMessageId(maybe.json.id)) { return false } @@ -83,13 +83,13 @@ export function isPeerEventStreamMessage(maybe: PeerMessage): maybe is PeerEvent return false } - if (maybe.json.retry !== undefined && !Number.isFinite(maybe.json.retry)) { + if (maybe.json.retry !== undefined && !isEventStreamMessageRetry(maybe.json.retry)) { return false } if ( maybe.json.comments !== undefined - && !(Array.isArray(maybe.json.comments) && maybe.json.comments.every(v => typeof v === 'string')) + && !(Array.isArray(maybe.json.comments) && maybe.json.comments.every(isEventStreamMessageComment)) ) { return false }