refactor: validation

This commit is contained in:
Carlos Valente
2023-12-01 13:08:05 +01:00
parent f720c56345
commit efcd6494be
20 changed files with 263 additions and 172 deletions
+5 -5
View File
@@ -3,12 +3,12 @@ import {
Alias, Alias,
DatabaseModel, DatabaseModel,
GetInfo, GetInfo,
HTTPSettings, HttpSettings,
OntimeRundown, OntimeRundown,
OSCSettings, OSCSettings,
OscSubscription,
ProjectData, ProjectData,
Settings, Settings,
Subscription,
UserFields, UserFields,
ViewSettings, ViewSettings,
} from 'ontime-types'; } from 'ontime-types';
@@ -109,7 +109,7 @@ export async function getOSC(): Promise<OSCSettings> {
* @description HTTP request to retrieve http settings * @description HTTP request to retrieve http settings
* @return {Promise} * @return {Promise}
*/ */
export async function getHTTP(): Promise<HTTPSettings> { export async function getHTTP(): Promise<HttpSettings> {
const res = await axios.get(`${ontimeURL}/http`); const res = await axios.get(`${ontimeURL}/http`);
return res.data; return res.data;
} }
@@ -118,7 +118,7 @@ export async function getHTTP(): Promise<HTTPSettings> {
* @description HTTP request to mutate http settings * @description HTTP request to mutate http settings
* @return {Promise} * @return {Promise}
*/ */
export async function postHTTP(data: HTTPSettings) { export async function postHTTP(data: HttpSettings) {
return axios.post(`${ontimeURL}/http`, data); return axios.post(`${ontimeURL}/http`, data);
} }
@@ -134,7 +134,7 @@ export async function postOSC(data: OSCSettings) {
* @description HTTP request to mutate osc subscriptions * @description HTTP request to mutate osc subscriptions
* @return {Promise} * @return {Promise}
*/ */
export async function postOscSubscriptions(data: Subscription) { export async function postOscSubscriptions(data: OscSubscription) {
return axios.post(`${ontimeURL}/osc-subscriptions`, data); return axios.post(`${ontimeURL}/osc-subscriptions`, data);
} }
@@ -1,5 +1,5 @@
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { HTTPSettings } from 'ontime-types'; import { HttpSettings } from 'ontime-types';
import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { HTTP_SETTINGS } from '../api/apiConstants'; import { HTTP_SETTINGS } from '../api/apiConstants';
@@ -20,7 +20,7 @@ export function useHttpSettings() {
}); });
// we need to jump through some hoops because of the type op port // we need to jump through some hoops because of the type op port
return { data: data! as unknown as HTTPSettings, status, isFetching, isError, refetch }; return { data: data! as unknown as HttpSettings, status, isFetching, isError, refetch };
} }
export function usePostHttpSettings() { export function usePostHttpSettings() {
+2 -2
View File
@@ -1,6 +1,6 @@
import { HTTPSettings } from 'ontime-types'; import { HttpSettings } from 'ontime-types';
export const httpPlaceholder: HTTPSettings = { export const httpPlaceholder: HttpSettings = {
enabledOut: false, enabledOut: false,
subscriptions: { subscriptions: {
onLoad: [], onLoad: [],
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { Switch } from '@chakra-ui/react'; import { Switch } from '@chakra-ui/react';
import type { HTTPSettings } from 'ontime-types'; import type { HttpSettings } from 'ontime-types';
import { TimerLifeCycle } from 'ontime-types'; import { TimerLifeCycle } from 'ontime-types';
import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings'; import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings';
@@ -24,7 +24,7 @@ export default function HttpIntegration() {
register, register,
reset, reset,
formState: { isSubmitting, isDirty, isValid }, formState: { isSubmitting, isDirty, isValid },
} = useForm<HTTPSettings>({ } = useForm<HttpSettings>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: data, defaultValues: data,
values: data, values: data,
@@ -45,9 +45,9 @@ export default function HttpIntegration() {
reset(data); reset(data);
}; };
const onSubmit = async (values: HTTPSettings) => { const onSubmit = async (values: HttpSettings) => {
try { try {
const newSettings: HTTPSettings = { const newSettings: HttpSettings = {
enabledOut: Boolean(values.enabledOut), enabledOut: Boolean(values.enabledOut),
subscriptions: { subscriptions: {
onLoad: values.subscriptions.onLoad ?? [], onLoad: values.subscriptions.onLoad ?? [],
@@ -59,8 +59,6 @@ export default function HttpIntegration() {
}, },
}; };
console.log('debug will submit', newSettings);
await mutateAsync(newSettings); await mutateAsync(newSettings);
} catch (error) { } catch (error) {
emitError(`Error setting HTML: ${error}`); emitError(`Error setting HTML: ${error}`);
@@ -2,7 +2,7 @@ import { Control, useFieldArray, UseFormRegister } from 'react-hook-form';
import { Button, IconButton, Input, Switch } from '@chakra-ui/react'; import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp'; import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove'; import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { HTTPSettings, TimerLifeCycle } from 'ontime-types'; import { HttpSettings, TimerLifeCycle } from 'ontime-types';
import { useEmitLog } from '../../../../common/stores/logger'; import { useEmitLog } from '../../../../common/stores/logger';
import { startsWithHttp } from '../../../../common/utils/regex'; import { startsWithHttp } from '../../../../common/utils/regex';
@@ -16,8 +16,8 @@ interface SubscriptionRowProps {
subtitle: string; subtitle: string;
visible: boolean; visible: boolean;
setShowSection: (cycle: TimerLifeCycle) => void; setShowSection: (cycle: TimerLifeCycle) => void;
register: UseFormRegister<HTTPSettings>; register: UseFormRegister<HttpSettings>;
control: Control<HTTPSettings>; control: Control<HttpSettings>;
placeholder: string; placeholder: string;
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import { HTTPSettings, LogOrigin, OSCSettings } from 'ontime-types'; import { HttpSettings, LogOrigin, OSCSettings } from 'ontime-types';
import 'dotenv/config'; import 'dotenv/config';
import express from 'express'; import express from 'express';
@@ -194,7 +194,7 @@ export const startOSCServer = async (overrideConfig = null) => {
/** /**
* starts integrations * starts integrations
*/ */
export const startIntegrations = async (config?: { osc: OSCSettings; http: HTTPSettings }) => { export const startIntegrations = async (config?: { osc: OSCSettings; http: HttpSettings }) => {
checkStart(OntimeStartOrder.InitIO); checkStart(OntimeStartOrder.InitIO);
const { osc, http } = config ?? DataProvider.getData(); const { osc, http } = config ?? DataProvider.getData();
@@ -1,5 +1,5 @@
import { LogOrigin } from 'ontime-types'; import { LogOrigin } from 'ontime-types';
import type { Alias, DatabaseModel, GetInfo, HTTPSettings, ProjectData } from 'ontime-types'; import type { Alias, DatabaseModel, GetInfo, HttpSettings, ProjectData } from 'ontime-types';
import { RequestHandler, Request, Response } from 'express'; import { RequestHandler, Request, Response } from 'express';
import fs from 'fs'; import fs from 'fs';
@@ -335,7 +335,7 @@ export const postOscSubscriptions = async (req, res) => {
}; };
// Create controller for GET request to '/ontime/http' // Create controller for GET request to '/ontime/http'
export const getHTTP = async (_req, res: Response<HTTPSettings>) => { export const getHTTP = async (_req, res: Response<HttpSettings>) => {
const http = DataProvider.getHttp(); const http = DataProvider.getHttp();
res.status(200).send(http); res.status(200).send(http);
}; };
@@ -1,5 +1,9 @@
import { body, check, validationResult } from 'express-validator'; import { body, check, validationResult } from 'express-validator';
import { validateOscObject, validateSubscriptionEntry } from '../utils/parserFunctions.js'; import {
validateHttpSubscriptionObject,
validateOscSubscriptionObject,
validateOscSubscriptionCycle,
} from '../utils/parserFunctions.js';
/** /**
* @description Validates object for POST /ontime/views * @description Validates object for POST /ontime/views
@@ -82,7 +86,7 @@ export const validateOSC = [
body('enabledOut').exists().isBoolean(), body('enabledOut').exists().isBoolean(),
body('subscriptions') body('subscriptions')
.isObject() .isObject()
.custom((value) => validateOscObject(value)), .custom((value) => validateOscSubscriptionObject(value)),
(req, res, next) => { (req, res, next) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
@@ -97,7 +101,7 @@ export const validateHTTP = [
body('enabledOut').exists().isBoolean(), body('enabledOut').exists().isBoolean(),
body('subscriptions') body('subscriptions')
.isObject() .isObject()
.custom((value) => validateOscObject(value)), .custom((value) => validateHttpSubscriptionObject(value)),
(req, res, next) => { (req, res, next) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
@@ -108,25 +112,25 @@ export const validateHTTP = [
/** /**
* @description Validates object for POST /ontime/osc-subscriptions * @description Validates object for POST /ontime/osc-subscriptions
*/ */
export const validateSubscription = [ export const validateOscSubscription = [
body('onLoad') body('onLoad')
.isArray() .isArray()
.custom((value) => validateSubscriptionEntry(value)), .custom((value) => validateOscSubscriptionCycle(value)),
body('onStart') body('onStart')
.isArray() .isArray()
.custom((value) => validateSubscriptionEntry(value)), .custom((value) => validateOscSubscriptionCycle(value)),
body('onPause') body('onPause')
.isArray() .isArray()
.custom((value) => validateSubscriptionEntry(value)), .custom((value) => validateOscSubscriptionCycle(value)),
body('onStop') body('onStop')
.isArray() .isArray()
.custom((value) => validateSubscriptionEntry(value)), .custom((value) => validateOscSubscriptionCycle(value)),
body('onUpdate') body('onUpdate')
.isArray() .isArray()
.custom((value) => validateSubscriptionEntry(value)), .custom((value) => validateOscSubscriptionCycle(value)),
body('onFinish') body('onFinish')
.isArray() .isArray()
.custom((value) => validateSubscriptionEntry(value)), .custom((value) => validateOscSubscriptionCycle(value)),
(req, res, next) => { (req, res, next) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
+2 -2
View File
@@ -26,7 +26,7 @@ import {
import { import {
validateAliases, validateAliases,
validateOSC, validateOSC,
validateSubscription, validateOscSubscriptionObject,
validatePatchProjectFile, validatePatchProjectFile,
validateSettings, validateSettings,
validateUserFields, validateUserFields,
@@ -86,7 +86,7 @@ router.get('/osc', getOSC);
router.post('/osc', validateOSC, postOSC); router.post('/osc', validateOSC, postOSC);
// create route between controller and '/ontime/osc-subscriptions' endpoint // create route between controller and '/ontime/osc-subscriptions' endpoint
router.post('/osc-subscriptions', validateSubscription, postOscSubscriptions); router.post('/osc-subscriptions', validateOscSubscriptionObject, postOscSubscriptions);
// create route between controller and '/ontime/http' endpoint // create route between controller and '/ontime/http' endpoint
router.get('/http', getHTTP); router.get('/http', getHTTP);
@@ -1,11 +1,11 @@
//TODO: cleanup stuff left over from OSC copy //TODO: cleanup stuff left over from OSC copy
import http from 'node:http'; import http from 'node:http';
import { HTTPSettings, LogOrigin, Subscription } from 'ontime-types'; import { HttpSettings, HttpSubscription, HttpSubscriptionOptions, LogOrigin } from 'ontime-types';
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js'; import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
import { parseTemplateNested } from './integrationUtils.js'; import { parseTemplateNested } from './integrationUtils.js';
import { dbModel } from '../../models/dataModel.js'; import { dbModel } from '../../models/dataModel.js';
import { validateHttpObject } from '../../utils/parserFunctions.js'; import { validateHttpSubscriptionEntry } from '../../utils/parserFunctions.js';
import { logger } from '../../classes/Logger.js'; import { logger } from '../../classes/Logger.js';
import { URL } from 'node:url'; import { URL } from 'node:url';
@@ -15,9 +15,9 @@ type Action = TimerLifeCycleKey | string;
* @description Class contains logic towards outgoing HTTP communications * @description Class contains logic towards outgoing HTTP communications
* @class * @class
*/ */
export class HttpIntegration implements IIntegration { export class HttpIntegration implements IIntegration<HttpSubscriptionOptions> {
protected httpAgent: null | http.Agent; protected httpAgent: null | http.Agent;
subscriptions: Subscription; subscriptions: HttpSubscription;
constructor() { constructor() {
this.httpAgent = null; this.httpAgent = null;
@@ -27,7 +27,7 @@ export class HttpIntegration implements IIntegration {
/** /**
* Initializes httpClient * Initializes httpClient
*/ */
init(config: HTTPSettings) { init(config: HttpSettings) {
const { subscriptions, enabledOut } = config; const { subscriptions, enabledOut } = config;
if (!enabledOut) { if (!enabledOut) {
@@ -60,8 +60,9 @@ export class HttpIntegration implements IIntegration {
} }
} }
initSubscriptions(subscriptionOptions: Subscription) { initSubscriptions(subscriptionOptions: HttpSubscription) {
if (validateHttpObject(subscriptionOptions)) { // TODO: here we have to validate the entire subscription object
if (validateHttpSubscriptionEntry(subscriptionOptions)) {
this.subscriptions = { ...subscriptionOptions }; this.subscriptions = { ...subscriptionOptions };
} }
} }
@@ -2,8 +2,8 @@ import { TimerLifeCycle, Subscription } from 'ontime-types';
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle; export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
export default interface IIntegration { export default interface IIntegration<T> {
subscriptions: Subscription; subscriptions: Subscription<T>;
init: (config: unknown) => OperationReturn; init: (config: unknown) => OperationReturn;
dispatch: (action: TimerLifeCycleKey, state?: object) => OperationReturn; dispatch: (action: TimerLifeCycleKey, state?: object) => OperationReturn;
emit: (...args: unknown[]) => unknown; emit: (...args: unknown[]) => unknown;
@@ -2,17 +2,17 @@ import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
import { eventStore } from '../../stores/EventStore.js'; import { eventStore } from '../../stores/EventStore.js';
class IntegrationService { class IntegrationService {
private integrations: IIntegration[]; private integrations: IIntegration<unknown>[];
constructor() { constructor() {
this.integrations = []; this.integrations = [];
} }
register(integrationService: IIntegration) { register(integrationService: IIntegration<unknown>) {
this.integrations.push(integrationService); this.integrations.push(integrationService);
} }
unregister(integrationService: IIntegration) { unregister(integrationService: IIntegration<unknown>) {
this.integrations = this.integrations.filter((int) => int !== integrationService); this.integrations = this.integrations.filter((int) => int !== integrationService);
} }
@@ -1,11 +1,11 @@
import { ArgumentType, Client, Message } from 'node-osc'; import { ArgumentType, Client, Message } from 'node-osc';
import { OSCSettings, Subscription } from 'ontime-types'; import { OSCSettings, OscSubscription, OscSubscriptionOptions } from 'ontime-types';
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js'; import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
import { parseTemplateNested } from './integrationUtils.js'; import { parseTemplateNested } from './integrationUtils.js';
import { isObject } from '../../utils/varUtils.js'; import { isObject } from '../../utils/varUtils.js';
import { dbModel } from '../../models/dataModel.js'; import { dbModel } from '../../models/dataModel.js';
import { validateOscObject } from '../../utils/parserFunctions.js'; import { validateOscSubscriptionObject } from '../../utils/parserFunctions.js';
type Action = TimerLifeCycleKey | string; type Action = TimerLifeCycleKey | string;
@@ -13,9 +13,9 @@ type Action = TimerLifeCycleKey | string;
* @description Class contains logic towards outgoing OSC communications * @description Class contains logic towards outgoing OSC communications
* @class * @class
*/ */
export class OscIntegration implements IIntegration { export class OscIntegration implements IIntegration<OscSubscriptionOptions> {
protected oscClient: null | Client; protected oscClient: null | Client;
subscriptions: Subscription; subscriptions: OscSubscription;
constructor() { constructor() {
this.oscClient = null; this.oscClient = null;
@@ -65,8 +65,8 @@ export class OscIntegration implements IIntegration {
} }
} }
initSubscriptions(subscriptionOptions: Subscription) { initSubscriptions(subscriptionOptions: OscSubscription) {
if (validateOscObject(subscriptionOptions)) { if (validateOscSubscriptionObject(subscriptionOptions)) {
this.subscriptions = { ...subscriptionOptions }; this.subscriptions = { ...subscriptionOptions };
} }
} }
@@ -1,91 +0,0 @@
import { validateOscObject } from '../parserFunctions.ts';
test('validateOscSubscription()', () => {
it('should return true when given a valid OscSubscription', () => {
const validSubscription = {
onLoad: [{ id: '1', message: 'test', enabled: true }],
onStart: [{ id: '2', message: 'test', enabled: false }],
onPause: [{ id: '3', message: 'test', enabled: true }],
onStop: [{ id: '4', message: 'test', enabled: false }],
onUpdate: [{ id: '5', message: 'test', enabled: true }],
onFinish: [{ id: '6', message: 'test', enabled: false }],
};
const result = validateOscObject(validSubscription);
expect(result).toBe(true);
});
it('should return false when given undefined', () => {
const result = validateOscObject(undefined);
expect(result).toBe(false);
});
it('should return false when given null', () => {
const result = validateOscObject(null);
expect(result).toBe(false);
});
it('should return false when given an empty object', () => {
const result = validateOscObject({});
expect(result).toBe(false);
});
it('should return false when given an empty array', () => {
const result = validateOscObject([]);
expect(result).toBe(false);
});
it('should return false when given an object that is not an OscSubscription', () => {
const invalidObject = { foo: 'bar' };
const result = validateOscObject(invalidObject);
expect(result).toBe(false);
});
it('should return false when given an OscSubscription with a missing property', () => {
const invalidSubscription = {
onLoad: [{ id: '1', message: 'test', enabled: true }],
onStart: [{ id: '2', message: 'test', enabled: false }],
onPause: [{ id: '3', message: 'test', enabled: true }],
// Missing onStop
onUpdate: [{ id: '5', message: 'test', enabled: true }],
onFinish: [{ id: '6', message: 'test', enabled: false }],
};
const result = validateOscObject(invalidSubscription);
expect(result).toBe(false);
});
it('should return false when given an OscSubscription with an invalid property value', () => {
const invalidSubscription = {
onLoad: [{ id: '1', message: 'test', enabled: true }],
onStart: [{ id: '2', message: 'test', enabled: false }],
onPause: [{ id: '3', message: 'test', enabled: true }],
onStop: [{ id: '4', message: 'test', enabled: false }],
onUpdate: [{ id: '5', message: 'test', enabled: true }],
onFinish: [{ id: '6', message: 'test', enabled: 'not a boolean' }],
};
const result = validateOscObject(invalidSubscription);
expect(result).toBe(false);
});
it('should return true if the message field is empty', () => {
const invalidSubscription = {
onLoad: [{ id: '1', message: 'test', enabled: true }],
onStart: [{ id: '2', message: '', enabled: false }],
onPause: [{ id: '3', message: '', enabled: true }],
onStop: [{ id: '4', message: 'test', enabled: false }],
onUpdate: [{ id: '5', message: 'test', enabled: true }],
onFinish: [{ id: '6', message: 'test', enabled: 'not a boolean' }],
};
const result = validateOscObject(invalidSubscription);
expect(result).toBe(true);
});
});
@@ -0,0 +1,161 @@
import { HttpSubscription, OscSubscription } from 'ontime-types';
import {
validateOscSubscriptionObject,
validateOscSubscriptionCycle,
validateHttpSubscriptionCycle,
validateHttpSubscriptionObject,
} from '../parserFunctions.js';
describe('validateOscSubscriptionCycle()', () => {
it('should return false when given an OscSubscription with an invalid property value', () => {
const invalidEntry = [{ message: 'test', enabled: 'not a boolean' }];
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
const result = validateOscSubscriptionCycle(invalidEntry);
expect(result).toBe(false);
});
});
describe('validateOscSubscriptionObject()', () => {
it('should return true when given a valid OscSubscription', () => {
const validSubscription: OscSubscription = {
onLoad: [{ message: 'test', enabled: true }],
onStart: [{ message: 'test', enabled: false }],
onPause: [{ message: 'test', enabled: true }],
onStop: [{ message: 'test', enabled: false }],
onUpdate: [{ message: 'test', enabled: true }],
onFinish: [{ message: 'test', enabled: false }],
};
const result = validateOscSubscriptionObject(validSubscription);
expect(result).toBe(true);
});
it('should return false when given undefined', () => {
const result = validateOscSubscriptionObject(undefined);
expect(result).toBe(false);
});
it('should return false when given null', () => {
const result = validateOscSubscriptionObject(null);
expect(result).toBe(false);
});
it('should return false when given an empty object', () => {
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
const result = validateOscSubscriptionObject({});
expect(result).toBe(false);
});
it('should return false when given an empty array', () => {
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
const result = validateOscSubscriptionObject([]);
expect(result).toBe(false);
});
it('should return false when given an object that is not an OscSubscription', () => {
const invalidObject = { foo: 'bar' };
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
const result = validateOscSubscriptionObject(invalidObject);
expect(result).toBe(false);
});
it('should return false when given an OscSubscription with a missing property', () => {
const invalidSubscription = {
onLoad: [{ message: 'test', enabled: true }],
onStart: [{ message: 'test', enabled: false }],
onPause: [{ message: 'test', enabled: true }],
// Missing onStop
onUpdate: [{ message: 'test', enabled: true }],
onFinish: [{ message: 'test', enabled: false }],
};
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
const result = validateOscSubscriptionObject(invalidSubscription);
expect(result).toBe(false);
});
});
describe('validateHttpSubscriptionCycle()', () => {
it('should return false when given an HttpSubscription with an invalid property value', () => {
const invalidBoolean = [{ message: 'http://', enabled: 'not a boolean' }];
const invalidHttp = [{ message: 'test', enabled: true }];
const noHttps = [{ message: 'https://test', enabled: true }];
const noEmpty = [{ message: '', enabled: true }];
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
expect(validateHttpSubscriptionCycle(invalidBoolean)).toBe(false);
expect(validateHttpSubscriptionCycle(invalidHttp)).toBe(false);
expect(validateHttpSubscriptionCycle(noHttps)).toBe(false);
expect(validateHttpSubscriptionCycle(noEmpty)).toBe(false);
});
it('should return true when given an HttpSubscription matches definition', () => {
const validHttp = [{ message: 'http://', enabled: true }];
const result = validateHttpSubscriptionCycle(validHttp);
expect(result).toBe(true);
});
});
describe('validateHttpSubscriptionObject()', () => {
it('should return true when given a valid HttpSubscription', () => {
const validSubscription: HttpSubscription = {
onLoad: [{ message: 'http://', enabled: true }],
onStart: [{ message: 'http://', enabled: false }],
onPause: [{ message: 'http://', enabled: true }],
onStop: [{ message: 'http://', enabled: false }],
onUpdate: [{ message: 'http://', enabled: true }],
onFinish: [{ message: 'http://', enabled: false }],
};
const result = validateHttpSubscriptionObject(validSubscription);
expect(result).toBe(true);
});
it('should return false when given undefined', () => {
const result = validateHttpSubscriptionObject(undefined);
expect(result).toBe(false);
});
it('should return false when given null', () => {
const result = validateHttpSubscriptionObject(null);
expect(result).toBe(false);
});
it('should return false when given an empty object', () => {
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
const result = validateOscSubscriptionObject({});
expect(result).toBe(false);
});
it('should return false when given an empty array', () => {
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
const result = validateHttpSubscriptionObject([]);
expect(result).toBe(false);
});
it('should return false when given an object that is not an HttpSubscription', () => {
const invalidObject = { foo: 'bar' };
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
const result = validateHttpSubscriptionObject(invalidObject);
expect(result).toBe(false);
});
it('should return false when given an HttpSubscription with a missing property', () => {
const invalidSubscription = {
onLoad: [{ message: 'http://', enabled: true }],
onStart: [{ message: 'http://', enabled: false }],
onPause: [{ message: 'http://', enabled: true }],
// Missing onStop
onUpdate: [{ message: 'http://', enabled: true }],
onFinish: [{ message: 'http://', enabled: false }],
};
// @ts-expect-error -- since this comes from the client, we check things that typescript would have caught
const result = validateHttpSubscriptionObject(invalidSubscription);
expect(result).toBe(false);
});
});
+39 -22
View File
@@ -2,15 +2,17 @@ import { generateId } from 'ontime-utils';
import { import {
Alias, Alias,
OntimeRundown, OntimeRundown,
HTTPSettings, HttpSettings,
OSCSettings, OSCSettings,
Subscription,
SubscriptionOptions,
ProjectData, ProjectData,
Settings, Settings,
TimerLifeCycle, TimerLifeCycle,
UserFields, UserFields,
ViewSettings, ViewSettings,
OscSubscription,
HttpSubscription,
OscSubscriptionOptions,
HttpSubscriptionOptions,
} from 'ontime-types'; } from 'ontime-types';
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js'; import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
@@ -160,12 +162,12 @@ export const parseViewSettings = (data): ViewSettings => {
}; };
/** /**
* Parses and validates subscription entry * Parses and validates OSC subscription cycle options
* @param data * @param data
*/ */
export const validateSubscriptionEntry = (data: SubscriptionOptions): boolean => { export const validateOscSubscriptionCycle = (data: OscSubscriptionOptions[]): boolean => {
for (const subscription in data) { for (const subscriptionOption of data) {
if (typeof data[subscription].message !== 'string' || typeof data[subscription].enabled !== 'boolean') { if (typeof subscriptionOption.message !== 'string' || typeof subscriptionOption.enabled !== 'boolean') {
return false; return false;
} }
} }
@@ -173,22 +175,23 @@ export const validateSubscriptionEntry = (data: SubscriptionOptions): boolean =>
}; };
/** /**
* Parses and validates subscription object * Parses and validates OSC subscription object
* @param data * @param data
*/ */
export const validateOscObject = (data: Subscription): boolean => { export const validateOscSubscriptionObject = (data: OscSubscription): boolean => {
if (!data) { if (!data) {
return false; return false;
} }
const timerKeys = Object.keys(TimerLifeCycle); const timerKeys = Object.keys(TimerLifeCycle);
for (const key of timerKeys) { for (const key of timerKeys) {
// must contains all keys and be an array
if (!(key in data) || !Array.isArray(data[key])) { if (!(key in data) || !Array.isArray(data[key])) {
return false; return false;
} }
for (const subscription of data[key]) { const isValid = validateOscSubscriptionCycle(data[key]);
if (typeof subscription.message !== 'string' || typeof subscription.enabled !== 'boolean') { if (!isValid) {
return false; return false;
}
} }
} }
return true; return true;
@@ -202,7 +205,7 @@ export const parseOsc = (data: { osc?: Partial<OSCSettings> }): OSCSettings => {
console.log('Found OSC definition, importing...'); console.log('Found OSC definition, importing...');
const loadedConfig = data.osc || {}; const loadedConfig = data.osc || {};
const validatedSubscriptions = validateOscObject(loadedConfig.subscriptions) const validatedSubscriptions = validateOscSubscriptionObject(loadedConfig.subscriptions)
? loadedConfig.subscriptions ? loadedConfig.subscriptions
: dbModel.osc.subscriptions; : dbModel.osc.subscriptions;
@@ -218,22 +221,36 @@ export const parseOsc = (data: { osc?: Partial<OSCSettings> }): OSCSettings => {
}; };
/** /**
* Parses and validates subscription object * Parses and validates HTTP subscription cycle options
* @param data * @param data
*/ */
export const validateHttpObject = (data: Subscription): boolean => { export const validateHttpSubscriptionCycle = (data: HttpSubscriptionOptions[]): boolean => {
for (const subscriptionOption of data) {
const isHttp = subscriptionOption.message?.startsWith('http://');
if (typeof subscriptionOption.message !== 'string' || !isHttp || typeof subscriptionOption.enabled !== 'boolean') {
return false;
}
}
return true;
};
/**
* Parses and validates HTTP subscription object
* @param data
*/
export const validateHttpSubscriptionObject = (data: HttpSubscription): boolean => {
if (!data) { if (!data) {
return false; return false;
} }
const timerKeys = Object.keys(TimerLifeCycle); const timerKeys = Object.keys(TimerLifeCycle);
// must contains all keys and be an array
for (const key of timerKeys) { for (const key of timerKeys) {
if (!(key in data) || !Array.isArray(data[key])) { if (!(key in data) || !Array.isArray(data[key])) {
return false; return false;
} }
for (const subscription of data[key]) { const isValid = validateHttpSubscriptionCycle(data[key]);
if (typeof subscription.message !== 'string' || typeof subscription.enabled !== 'boolean') { if (!isValid) {
return false; return false;
}
} }
} }
return true; return true;
@@ -245,12 +262,12 @@ export const validateHttpObject = (data: Subscription): boolean => {
* @param {boolean} enforce - whether to create a definition if one is missing * @param {boolean} enforce - whether to create a definition if one is missing
* @returns {object} - event object data * @returns {object} - event object data
*/ */
export const parseHttp = (data: { http?: Partial<HTTPSettings> }): HTTPSettings => { export const parseHttp = (data: { http?: Partial<HttpSettings> }): HttpSettings => {
if ('http' in data) { if ('http' in data) {
console.log('Found HTTP definition, importing...'); console.log('Found HTTP definition, importing...');
const loadedConfig = data?.http || {}; const loadedConfig = data?.http || {};
const validatedSubscriptions = validateHttpObject(loadedConfig.subscriptions) const validatedSubscriptions = validateHttpSubscriptionObject(loadedConfig.subscriptions)
? loadedConfig.subscriptions ? loadedConfig.subscriptions
: dbModel.http.subscriptions; : dbModel.http.subscriptions;
@@ -5,7 +5,7 @@ import { OSCSettings } from './core/OscSettings.type.js';
import { Settings } from './core/Settings.type.js'; import { Settings } from './core/Settings.type.js';
import { UserFields } from './core/UserFields.type.js'; import { UserFields } from './core/UserFields.type.js';
import { ViewSettings } from './core/Views.type.js'; import { ViewSettings } from './core/Views.type.js';
import { HTTPSettings } from '../index.js'; import { HttpSettings } from '../index.js';
export type DatabaseModel = { export type DatabaseModel = {
rundown: OntimeRundown; rundown: OntimeRundown;
@@ -15,5 +15,5 @@ export type DatabaseModel = {
aliases: Alias[]; aliases: Alias[];
userFields: UserFields; userFields: UserFields;
osc: OSCSettings; osc: OSCSettings;
http: HTTPSettings; http: HttpSettings;
}; };
@@ -1,9 +1,9 @@
import { Subscription } from './Subscription.type.js'; import { Subscription } from './Subscription.type.js';
type HttpSubscriptionOptions = { message: string; enabled: boolean }; export type HttpSubscriptionOptions = { message: string; enabled: boolean };
export type HttpSubscription = Subscription<HttpSubscriptionOptions>; export type HttpSubscription = Subscription<HttpSubscriptionOptions>;
export interface HTTPSettings { export interface HttpSettings {
enabledOut: boolean; enabledOut: boolean;
subscriptions: HttpSubscription; subscriptions: HttpSubscription;
} }
@@ -1,6 +1,6 @@
import { Subscription } from './Subscription.type.js'; import { Subscription } from './Subscription.type.js';
type OscSubscriptionOptions = { message: string; enabled: boolean }; export type OscSubscriptionOptions = { message: string; enabled: boolean };
export type OscSubscription = Subscription<OscSubscriptionOptions>; export type OscSubscription = Subscription<OscSubscriptionOptions>;
export interface OSCSettings { export interface OSCSettings {
+3 -2
View File
@@ -32,9 +32,10 @@ export type { UserFields } from './definitions/core/UserFields.type.js';
export type { Subscription } from './definitions/core/Subscription.type.js'; export type { Subscription } from './definitions/core/Subscription.type.js';
// ---> OSC // ---> OSC
export type { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js'; export type { OSCSettings, OscSubscription, OscSubscriptionOptions } from './definitions/core/OscSettings.type.js';
// ---> HTTP // ---> HTTP
export type { HTTPSettings, HttpSubscription } from './definitions/core/HttpSettings.type.js'; export type { HttpSettings, HttpSubscription, HttpSubscriptionOptions } from './definitions/core/HttpSettings.type.js';
// SERVER RESPONSES // SERVER RESPONSES
export type { NetworkInterface, GetInfo } from './api/ontime-controller/BackendResponse.type.js'; export type { NetworkInterface, GetInfo } from './api/ontime-controller/BackendResponse.type.js';