diff --git a/apps/server/src/api-integration/__test__/integration.utils.test.ts b/apps/server/src/api-integration/__test__/integration.utils.test.ts new file mode 100644 index 000000000..4b9c6a1d6 --- /dev/null +++ b/apps/server/src/api-integration/__test__/integration.utils.test.ts @@ -0,0 +1,91 @@ +import { EntryCustomFields, OntimeEvent } from 'ontime-types'; +import { isValidChangeProperty } from '../integration.utils.js'; + +describe('isValidChangeProperty()', () => { + test('correct value and property', () => { + const testEvent = { + id: 'test', + duration: 111, + } as OntimeEvent; + + expect(isValidChangeProperty(testEvent, 'duration', 123)).toBeTruthy(); + }); + + test('correct property and undefined value', () => { + const testEvent = { + id: 'test', + duration: 111, + } as OntimeEvent; + + expect(isValidChangeProperty(testEvent, 'duration', undefined)).toBeFalsy(); + }); + + test('missing property and undefined value', () => { + const testEvent = { + id: 'test', + duration: 111, + } as OntimeEvent; + + expect(isValidChangeProperty(testEvent, 'missing', 123)).toBeFalsy(); + }); + + test('non existing custom value', () => { + const testEvent = { + id: 'test', + duration: 111, + custom: { + field: 'test', + } as EntryCustomFields, + } as OntimeEvent; + + expect(isValidChangeProperty(testEvent, 'custom:test', 123)).toBeFalsy(); + }); + + test('existing custom value', () => { + const testEvent = { + id: 'test', + duration: 111, + custom: { + test: 'test', + } as EntryCustomFields, + } as OntimeEvent; + + expect(isValidChangeProperty(testEvent, 'custom:test', 123)).toBeTruthy(); + }); + + test('empty custom definition', () => { + const testEvent = { + id: 'test', + duration: 111, + custom: { + test: 'test', + } as EntryCustomFields, + } as OntimeEvent; + + expect(isValidChangeProperty(testEvent, 'custom:', 123)).toBeFalsy(); + }); + + test('build-in in custom', () => { + const testEvent = { + id: 'test', + duration: 111, + custom: { + test: 'test', + } as EntryCustomFields, + } as OntimeEvent; + + expect(isValidChangeProperty(testEvent, 'custom:toString', 123)).toBeFalsy(); + }); + + test('build-in in top object', () => { + const testEvent = { + id: 'test', + duration: 111, + custom: { + test: 'test', + } as EntryCustomFields, + } as OntimeEvent; + + expect(isValidChangeProperty(testEvent, 'toString', 123)).toBeFalsy(); + }); +}); diff --git a/apps/server/src/api-integration/integration.controller.ts b/apps/server/src/api-integration/integration.controller.ts index 250cd15b0..52290fade 100644 --- a/apps/server/src/api-integration/integration.controller.ts +++ b/apps/server/src/api-integration/integration.controller.ts @@ -18,7 +18,7 @@ import { validateMessage, validateTimerMessage } from '../services/message-servi import { runtimeService } from '../services/runtime-service/runtime.service.js'; import { eventStore } from '../stores/EventStore.js'; import * as assert from '../utils/assert.js'; -import { parseProperty } from './integration.utils.js'; +import { parseProperty, isValidChangeProperty } from './integration.utils.js'; import { socket } from '../adapters/WebsocketAdapter.js'; import { throttle } from '../utils/throttle.js'; import { coerceEnum } from '../utils/coerceType.js'; @@ -77,10 +77,9 @@ const actionHandlers: Record = { let shouldThrottle = false; Object.entries(data).forEach(([property, value]) => { - if (typeof property !== 'string' || value === undefined || !(property in targetEntry)) { + if (!isValidChangeProperty(targetEntry, property, value)) { throw new Error('Invalid property or value'); } - // parseProperty is async because of the data lock const newObjectProperty = parseProperty(property, value); const key = Object.keys(newObjectProperty)[0]; shouldThrottle = shouldThrottle || willCauseRegeneration(key); diff --git a/apps/server/src/api-integration/integration.utils.ts b/apps/server/src/api-integration/integration.utils.ts index 53a377441..d2bb401c3 100644 --- a/apps/server/src/api-integration/integration.utils.ts +++ b/apps/server/src/api-integration/integration.utils.ts @@ -1,4 +1,4 @@ -import { EndAction, TimeStrategy, TimerType, isKeyOfType } from 'ontime-types'; +import { EndAction, OntimeEntry, TimeStrategy, TimerType, isKeyOfType } from 'ontime-types'; import { maxDuration } from 'ontime-utils'; import { coerceBoolean, coerceColour, coerceEnum, coerceNumber, coerceString } from '../utils/coerceType.js'; @@ -60,3 +60,14 @@ export function parseProperty(property: string, value: unknown) { const parserFn = propertyConversion[property]; return { [property]: parserFn(value) }; } + +export function isValidChangeProperty(target: OntimeEntry, property: string, value: unknown): boolean { + if (typeof property !== 'string') return false; + if (value === undefined) return false; + if (property.startsWith('custom:') && 'custom' in target) { + const customProperty = property.slice('custom:'.length); + if (!customProperty) return false; + return Object.hasOwn(target.custom, customProperty); + } + return Object.hasOwn(target, property); +}