From 4c2c7c24e9f31a663e6f4f440261bf576656072a Mon Sep 17 00:00:00 2001 From: Juan Pablo Lo Coco Date: Tue, 30 Jan 2024 11:06:38 -0300 Subject: [PATCH 1/5] chore: fix storyID parameter on roStoryMove --- packages/connector/src/MosDevice.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/connector/src/MosDevice.ts b/packages/connector/src/MosDevice.ts index 0ce048cf..bcfe6893 100644 --- a/packages/connector/src/MosDevice.ts +++ b/packages/connector/src/MosDevice.ts @@ -345,14 +345,18 @@ export class MosDevice implements IMOSDevice { }, } } else if (data.roStoryMove) { + const storyIDs: string[] = Array.isArray(data.roStoryMove.storyID) + ? (data.roStoryMove.storyID as string[]) + : [data.roStoryMove.storyID as string] + data.roElementAction = { roID: data.roStoryMove.roID, operation: 'MOVE', element_target: { - storyID: data.roStoryMove.storyID[1], + storyID: storyIDs[1], }, element_source: { - storyID: data.roStoryMove.storyID[0], + storyID: storyIDs[0], }, } } else if (data.roStorySwap) { From 5b997ef70142909e2ad243ff0011220626bea6cf Mon Sep 17 00:00:00 2001 From: Juan Pablo Lo Coco Date: Thu, 1 Feb 2024 00:39:58 -0300 Subject: [PATCH 2/5] chore: fix roStoryMoveMultiple and roItemMoveMultiple when sending 1 story or item --- packages/connector/src/MosDevice.ts | 34 +++++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/connector/src/MosDevice.ts b/packages/connector/src/MosDevice.ts index bcfe6893..79c8895c 100644 --- a/packages/connector/src/MosDevice.ts +++ b/packages/connector/src/MosDevice.ts @@ -378,20 +378,25 @@ export class MosDevice implements IMOSDevice { storyID: data.roStoryDelete.storyID, }, } - } else if (data.roStoryMoveMultiple && data.roStoryMoveMultiple.storyID.length > 1) { - const l = data.roStoryMoveMultiple.storyID.length + } else if (data.roStoryMoveMultiple && data.roStoryMoveMultiple.storyID) { + const stories: string[] = Array.isArray(data.roStoryMoveMultiple.storyID) + ? (data.roStoryMoveMultiple.storyID as string[]) + : [data.roStoryMoveMultiple.storyID as string] - const target = data.roStoryMoveMultiple.storyID[l - 1] - const sources = data.roStoryMoveMultiple.storyID.slice(0, l - 1) + // An aditional validation checking the length of stories can be added + const l = stories.length + + const target = stories[l - 1] + const sources = stories.slice(0, l - 1) data.roElementAction = { roID: data.roStoryMoveMultiple.roID, operation: 'MOVE', element_target: { - storyID: target, + storyID: l === 1 ? undefined : target, }, element_source: { - storyID: sources, + storyID: l === 1 ? stories : sources, }, } } else if (data.roItemInsert) { @@ -429,21 +434,26 @@ export class MosDevice implements IMOSDevice { itemID: data.roItemDelete.itemID, }, } - } else if (data.roItemMoveMultiple && data.roItemMoveMultiple.itemID.length > 1) { - const l = data.roItemMoveMultiple.itemID.length + } else if (data.roItemMoveMultiple && data.roItemMoveMultiple.itemID && data.roItemMoveMultiple.storyID) { + const items: string[] = Array.isArray(data.roItemMoveMultiple.itemID) + ? (data.roItemMoveMultiple.itemID as string[]) + : [data.roItemMoveMultiple.itemID as string] + + // An aditional validation checking the length of items can be added + const l = items.length - const target = data.roItemMoveMultiple.itemID[l - 1] - const sources = data.roItemMoveMultiple.itemID.slice(0, l - 1) + const target = items[l - 1] + const sources = items.slice(0, l - 1) data.roElementAction = { roID: data.roItemMoveMultiple.roID, operation: 'MOVE', element_target: { storyID: data.roItemMoveMultiple.storyID, - itemID: target, + itemID: l === 1 ? undefined : target, }, element_source: { - itemID: sources, + itemID: l === 1 ? items : sources, }, } } From 58ff30429976655b30596181041449b3e8060ff9 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Thu, 1 Feb 2024 09:23:55 +0100 Subject: [PATCH 3/5] fix: roStoryMove: off-spec support of single storyID tag --- packages/connector/src/MosDevice.ts | 29 +- packages/connector/src/__mocks__/testData.ts | 6 + .../src/__tests__/Profile2-non-strict.spec.ts | 281 ++++++++++++++++++ .../connector/src/__tests__/Profile2.spec.ts | 21 ++ .../Profile2-non-strict.spec.ts.snap | 41 +++ packages/connector/src/__tests__/lib.ts | 10 +- 6 files changed, 383 insertions(+), 5 deletions(-) create mode 100644 packages/connector/src/__tests__/Profile2-non-strict.spec.ts create mode 100644 packages/connector/src/__tests__/__snapshots__/Profile2-non-strict.spec.ts.snap diff --git a/packages/connector/src/MosDevice.ts b/packages/connector/src/MosDevice.ts index 79c8895c..4682abb1 100644 --- a/packages/connector/src/MosDevice.ts +++ b/packages/connector/src/MosDevice.ts @@ -345,9 +345,32 @@ export class MosDevice implements IMOSDevice { }, } } else if (data.roStoryMove) { - const storyIDs: string[] = Array.isArray(data.roStoryMove.storyID) - ? (data.roStoryMove.storyID as string[]) - : [data.roStoryMove.storyID as string] + // From documentation: + // **Note**: If the second tag is blank move the story to the bottom of the Running Order. + + let storyIDs: string[] + + if (Array.isArray(data.roStoryMove.storyID)) { + storyIDs = data.roStoryMove.storyID + } else { + if (this.strict) { + // The storyID is xml-converted to a string if the second tag is missing. + // The spec says that there must be two storyID tags, so we'll throw an error here: + return new MosModel.ROAck( + { + ID: this.mosTypes.mosString128.create(data.roStoryMove.roID), + Status: this.mosTypes.mosString128.create( + `The second tag is missing in .` + ), + Stories: [], + }, + this.strict + ) + } else { + // Non strict mode: This is technically out of spec, but it's a common mistake, so we'll handle it like so: + storyIDs = [data.roStoryMove.storyID as string, ''] + } + } data.roElementAction = { roID: data.roStoryMove.roID, diff --git a/packages/connector/src/__mocks__/testData.ts b/packages/connector/src/__mocks__/testData.ts index 4374d382..eeef8b9d 100644 --- a/packages/connector/src/__mocks__/testData.ts +++ b/packages/connector/src/__mocks__/testData.ts @@ -112,6 +112,8 @@ const xmlData = { roStoryInsert: `5PM HOTEL FIRE V: BRIDGE COLLAPSE Bridge Collapse B7 30848 M000627 testmos.enps.com\\\\server\\media\\clip392028cd2320s0d.mxfhttp://server/proxy/clipe.wmv 0 815 310 c01/l04/dve07 r00 PLAYLIST http://MOSA4.com/mos/supported_schemas/MOSAXML2.08 SHOLMES 2 463 a b PLAYLIST http://MOSA4.com/mos/supported_schemas/MOSBXML2.08 52 2 463 30849 M000628 testmos 0 815 310 c01/l04/dve07 r00 PLAYLIST http://MOSA4.com/mos/supported_schemas/MOSAXML2.08 SHOLMES 2 463 a b `, roStoryReplace: `5PM P: PHILLIPS INTERVIEW V: HOTEL FIRE Hotel Fire C1 30848 Hotel Fire vo M000702 testmos\\\\server\\media\\clip392028cd2320s0d.mxfhttp://server/proxy/clipe.wmv 0 900 800 c01/l04/dve07 r00 PLAYLIST http://MOSA4.com/mos/supported_schemas/MOSAXML2.08 SHOLMES 2 463 a b V: DORMITORY FIRE Dormitory Fire C2 1 Dormitory Fire vo M000705 testmos 0 800 310 c01/l04/dve07 r00 PLAYLIST http://MOSA4.com/mos/supported_schemas/MOSAXML2.08 SHOLMES 2 463 a b `, roStoryMove: `5PM V: BRIDGE COLLAPSE P: PHILLIPS INTERVIEW `, + roStoryMove_blank: `5PM V: BRIDGE COLLAPSE `, + roStoryMove_offspec_missing: `5PM V: BRIDGE COLLAPSE`, roStorySwap: `5PM V: BRIDGE COLLAPSE P: PHILLIPS INTERVIEW `, roStoryDelete: `5PM V: BRIDGE COLLAPSE P: PHILLIPS INTERVIEW `, roStoryMoveMultiple: `5PM 2 3 5 6 1 `, @@ -1523,6 +1525,10 @@ const xmlApiData = { StoryID: mosTypes.mosString128.create('P: PHILLIPS INTERVIEW'), }), roElementAction_roStoryMove_stories: [mosTypes.mosString128.create('V: BRIDGE COLLAPSE')], + roElementAction_roStoryMove_blank_action: literal({ + RunningOrderID: mosTypes.mosString128.create('5PM'), + StoryID: mosTypes.mosString128.create(''), + }), roElementAction_roStorySwap_action: literal({ RunningOrderID: mosTypes.mosString128.create('5PM'), }), diff --git a/packages/connector/src/__tests__/Profile2-non-strict.spec.ts b/packages/connector/src/__tests__/Profile2-non-strict.spec.ts new file mode 100644 index 00000000..a076c296 --- /dev/null +++ b/packages/connector/src/__tests__/Profile2-non-strict.spec.ts @@ -0,0 +1,281 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import { + checkReplyToServer, + clearMocks, + decode, + doBeforeAll, + encode, + fakeIncomingMessage, + fixSnapshot, + getMessageId, + getMosConnection, + getMosDevice, + getXMLReply, + mosTypes, + setupMocks, +} from './lib' +import { + MosConnection, + MosDevice, + IMOSObject, + IMOSItem, + IMOSItemAction, + IMOSItemStatus, + IMOSROAck, + IMOSROAction, + IMOSROReadyToAir, + IMOSROStory, + IMOSRunningOrder, + IMOSRunningOrderBase, + IMOSRunningOrderStatus, + IMOSStoryAction, + IMOSStoryStatus, + IMOSListMachInfo, + IMOSString128, +} from '..' +import { SocketMock } from '../__mocks__/socket' +import { ServerMock } from '../__mocks__/server' +import { xmlData, xmlApiData } from '../__mocks__/testData' + +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-ignore imports are unused +import { Socket } from 'net' +/* eslint-enable @typescript-eslint/no-unused-vars */ + +beforeAll(() => { + setupMocks() +}) +beforeEach(() => { + clearMocks() +}) +describe('Profile 2 - non strict', () => { + let mosDevice: MosDevice + let mosConnection: MosConnection + let socketMockLower: SocketMock + let socketMockUpper: SocketMock + + let serverSocketMockLower: SocketMock + let serverSocketMockUpper: SocketMock + let serverSocketMockQuery: SocketMock + + let onRequestMachineInfo: jest.Mock + let onRequestMOSObject: jest.Mock + let onRequestAllMOSObjects: jest.Mock + let onCreateRunningOrder: jest.Mock + let onReplaceRunningOrder: jest.Mock + let onDeleteRunningOrder: jest.Mock + let onRequestRunningOrder: jest.Mock + let onMetadataReplace: jest.Mock + let onRunningOrderStatus: jest.Mock + let onStoryStatus: jest.Mock + let onItemStatus: jest.Mock + let onReadyToAir: jest.Mock + let onROInsertStories: jest.Mock + let onROInsertItems: jest.Mock + let onROReplaceStories: jest.Mock + let onROReplaceItems: jest.Mock + let onROMoveStories: jest.Mock + let onROMoveItems: jest.Mock + let onRODeleteStories: jest.Mock + let onRODeleteItems: jest.Mock + let onROSwapStories: jest.Mock + let onROSwapItems: jest.Mock + + const mockReplyRoAck = jest.fn((data) => { + const str = decode(data) + const messageID = getMessageId(str) + return encode(getXMLReply(messageID, xmlData.roAck)) + }) + + beforeAll(async () => { + SocketMock.mockClear() + ServerMock.mockClear() + + mosConnection = await getMosConnection( + { + '0': true, + '1': true, + '2': true, + }, + false + ) + mosDevice = await getMosDevice(mosConnection) + + // Profile 0: + onRequestMachineInfo = jest.fn(async () => { + return xmlApiData.machineInfo + }) + mosDevice.onRequestMachineInfo(async (): Promise => { + return onRequestMachineInfo() + }) + // Profile 1: + onRequestMOSObject = jest.fn() + onRequestAllMOSObjects = jest.fn() + mosDevice.onRequestMOSObject(async (objId: string): Promise => { + return onRequestMOSObject(objId) + }) + mosDevice.onRequestAllMOSObjects(async (): Promise> => { + return onRequestAllMOSObjects() + }) + + // Profile 2: + const roAckReply = async () => { + const ack: IMOSROAck = { + ID: mosTypes.mosString128.create('runningOrderId'), + Status: mosTypes.mosString128.create('OK'), + Stories: [], + } + return ack + } + onCreateRunningOrder = jest.fn(roAckReply) + onReplaceRunningOrder = jest.fn(roAckReply) + onDeleteRunningOrder = jest.fn(roAckReply) + onRequestRunningOrder = jest.fn(async () => { + return xmlApiData.roCreate + }) + onMetadataReplace = jest.fn(roAckReply) + onRunningOrderStatus = jest.fn(roAckReply) + onStoryStatus = jest.fn(roAckReply) + onItemStatus = jest.fn(roAckReply) + onReadyToAir = jest.fn(roAckReply) + onROInsertStories = jest.fn(roAckReply) + onROInsertItems = jest.fn(roAckReply) + onROReplaceStories = jest.fn(roAckReply) + onROReplaceItems = jest.fn(roAckReply) + onROMoveStories = jest.fn(roAckReply) + onROMoveItems = jest.fn(roAckReply) + onRODeleteStories = jest.fn(roAckReply) + onRODeleteItems = jest.fn(roAckReply) + onROSwapStories = jest.fn(roAckReply) + onROSwapItems = jest.fn(roAckReply) + + mosDevice.onCreateRunningOrder(async (ro: IMOSRunningOrder): Promise => { + return onCreateRunningOrder(ro) + }) + mosDevice.onReplaceRunningOrder(async (ro: IMOSRunningOrder): Promise => { + return onReplaceRunningOrder(ro) + }) + mosDevice.onDeleteRunningOrder(async (runningOrderId: IMOSString128): Promise => { + return onDeleteRunningOrder(runningOrderId) + }) + mosDevice.onRequestRunningOrder(async (runningOrderId: IMOSString128): Promise => { + return onRequestRunningOrder(runningOrderId) + }) + mosDevice.onMetadataReplace(async (metadata: IMOSRunningOrderBase): Promise => { + return onMetadataReplace(metadata) + }) + mosDevice.onRunningOrderStatus(async (status: IMOSRunningOrderStatus): Promise => { + return onRunningOrderStatus(status) + }) + mosDevice.onStoryStatus(async (status: IMOSStoryStatus): Promise => { + return onStoryStatus(status) + }) + mosDevice.onItemStatus(async (status: IMOSItemStatus): Promise => { + return onItemStatus(status) + }) + mosDevice.onReadyToAir(async (Action: IMOSROReadyToAir): Promise => { + return onReadyToAir(Action) + }) + mosDevice.onROInsertStories( + async (Action: IMOSStoryAction, Stories: Array): Promise => { + return onROInsertStories(Action, Stories) + } + ) + mosDevice.onROInsertItems(async (Action: IMOSItemAction, Items: Array): Promise => { + return onROInsertItems(Action, Items) + }) + mosDevice.onROReplaceStories( + async (Action: IMOSStoryAction, Stories: Array): Promise => { + return onROReplaceStories(Action, Stories) + } + ) + mosDevice.onROReplaceItems(async (Action: IMOSItemAction, Items: Array): Promise => { + return onROReplaceItems(Action, Items) + }) + mosDevice.onROMoveStories( + async (Action: IMOSStoryAction, Stories: Array): Promise => { + return onROMoveStories(Action, Stories) + } + ) + mosDevice.onROMoveItems(async (Action: IMOSItemAction, Items: Array): Promise => { + return onROMoveItems(Action, Items) + }) + mosDevice.onRODeleteStories(async (Action: IMOSROAction, Stories: Array): Promise => { + return onRODeleteStories(Action, Stories) + }) + mosDevice.onRODeleteItems(async (Action: IMOSStoryAction, Items: Array): Promise => { + return onRODeleteItems(Action, Items) + }) + mosDevice.onROSwapStories( + async (Action: IMOSROAction, StoryID0: IMOSString128, StoryID1: IMOSString128): Promise => { + return onROSwapStories(Action, StoryID0, StoryID1) + } + ) + mosDevice.onROSwapItems( + async (Action: IMOSStoryAction, ItemID0: IMOSString128, ItemID1: IMOSString128): Promise => { + return onROSwapItems(Action, ItemID0, ItemID1) + } + ) + const b = doBeforeAll() + socketMockLower = b.socketMockLower + socketMockUpper = b.socketMockUpper + serverSocketMockLower = b.serverSocketMockLower + serverSocketMockUpper = b.serverSocketMockUpper + serverSocketMockQuery = b.serverSocketMockQuery + + mosConnection.checkProfileValidness() + mosDevice.checkProfileValidness() + }) + beforeEach(() => { + onRequestMOSObject.mockClear() + onRequestAllMOSObjects.mockClear() + + onCreateRunningOrder.mockClear() + onReplaceRunningOrder.mockClear() + onDeleteRunningOrder.mockClear() + onRequestRunningOrder.mockClear() + onMetadataReplace.mockClear() + onRunningOrderStatus.mockClear() + onStoryStatus.mockClear() + onItemStatus.mockClear() + onReadyToAir.mockClear() + onROInsertStories.mockClear() + onROInsertItems.mockClear() + onROReplaceStories.mockClear() + onROReplaceItems.mockClear() + onROMoveStories.mockClear() + onROMoveItems.mockClear() + onRODeleteStories.mockClear() + onRODeleteItems.mockClear() + onROSwapStories.mockClear() + onROSwapItems.mockClear() + + socketMockLower.mockClear() + socketMockUpper.mockClear() + serverSocketMockLower.mockClear() + serverSocketMockUpper.mockClear() + serverSocketMockQuery.mockClear() + + mockReplyRoAck.mockClear() + }) + afterAll(async () => { + await mosDevice.dispose() + await mosConnection.dispose() + }) + describe('deprecated messages', () => { + // These methods are still supported, but will be removed in future versions of the mos protocol + test('roStoryMove - missing second ', async () => { + // Note: from documentation: + // https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOS_Protocol_Version_2.8.5_Final.htm#roStoryMove + // **Note**: If the second tag is blank move the story to the bottom of the Running Order. + + // Fake incoming message on socket: + const messageId = await fakeIncomingMessage(serverSocketMockLower, xmlData.roStoryMove_offspec_missing) + expect(onROMoveStories).toHaveBeenCalledTimes(1) + expect(onROMoveStories.mock.calls[0][0]).toEqual(xmlApiData.roElementAction_roStoryMove_blank_action) + expect(onROMoveStories.mock.calls[0][1]).toEqual(xmlApiData.roElementAction_roStoryMove_stories) + expect(fixSnapshot(onROMoveStories.mock.calls)).toMatchSnapshot() + await checkReplyToServer(serverSocketMockLower, messageId, '') + }) + }) +}) diff --git a/packages/connector/src/__tests__/Profile2.spec.ts b/packages/connector/src/__tests__/Profile2.spec.ts index 1a837781..2ec3096b 100644 --- a/packages/connector/src/__tests__/Profile2.spec.ts +++ b/packages/connector/src/__tests__/Profile2.spec.ts @@ -1170,6 +1170,27 @@ describe('Profile 2', () => { expect(fixSnapshot(onROMoveStories.mock.calls)).toMatchSnapshot() await checkReplyToServer(serverSocketMockLower, messageId, '') }) + test('roStoryMove with blank', async () => { + // Fake incoming message on socket: + const messageId = await fakeIncomingMessage(serverSocketMockLower, xmlData.roStoryMove_blank) + expect(onROMoveStories).toHaveBeenCalledTimes(1) + expect(onROMoveStories.mock.calls[0][0]).toEqual(xmlApiData.roElementAction_roStoryMove_blank_action) + expect(onROMoveStories.mock.calls[0][1]).toEqual(xmlApiData.roElementAction_roStoryMove_stories) + expect(fixSnapshot(onROMoveStories.mock.calls)).toMatchSnapshot() + await checkReplyToServer(serverSocketMockLower, messageId, '') + }) + test('roStoryMove - missing second ', async () => { + // Note: from documentation: + // https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOS_Protocol_Version_2.8.5_Final.htm#roStoryMove + // **Note**: If the second tag is blank move the story to the bottom of the Running Order. + + // Fake incoming message on socket: + const messageId = await fakeIncomingMessage(serverSocketMockLower, xmlData.roStoryMove_offspec_missing) + expect(onROMoveStories).toHaveBeenCalledTimes(0) + + // This is out of spec: + await checkReplyToServer(serverSocketMockLower, messageId, '', 'The second', 'tag is missing') + }) test('roStorySwap', async () => { // Fake incoming message on socket: const messageId = await fakeIncomingMessage(serverSocketMockLower, xmlData.roStorySwap) diff --git a/packages/connector/src/__tests__/__snapshots__/Profile2-non-strict.spec.ts.snap b/packages/connector/src/__tests__/__snapshots__/Profile2-non-strict.spec.ts.snap new file mode 100644 index 00000000..14ce1fc0 --- /dev/null +++ b/packages/connector/src/__tests__/__snapshots__/Profile2-non-strict.spec.ts.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Profile 2 - non strict deprecated messages roStoryMove - missing second 1`] = ` +[ + [ + { + "RunningOrderID": { + "_mosString128": "5PM", + }, + "StoryID": { + "_mosString128": "", + }, + }, + [ + { + "_mosString128": "V: BRIDGE COLLAPSE", + }, + ], + ], +] +`; + +exports[`Profile 2 - non strict deprecated messages roStoryMove - missing second : + their.mos.id + our.mos.id + 1633 + + runningOrderId + OK + + 1`] = ` +" + their.mos.id + our.mos.id + 1633 + + runningOrderId + OK + +" +`; diff --git a/packages/connector/src/__tests__/lib.ts b/packages/connector/src/__tests__/lib.ts index 91ed3b74..ab19b029 100644 --- a/packages/connector/src/__tests__/lib.ts +++ b/packages/connector/src/__tests__/lib.ts @@ -141,7 +141,11 @@ export function decode(data: Buffer): string { export function encode(str: string): Buffer { return iconv.encode(str, 'utf16-be') } -export async function checkReplyToServer(socket: SocketMock, messageId: number, replyString: string): Promise { +export async function checkReplyToServer( + socket: SocketMock, + messageId: number, + ...replyStrings: string[] +): Promise { // check reply to server: await socket.mockWaitForSentMessages() @@ -149,7 +153,9 @@ export async function checkReplyToServer(socket: SocketMock, messageId: number, // @ts-ignore mock const reply = decode(socket.mockSentMessage.mock.calls[0][0]) expect(reply).toContain('' + messageId + '') - expect(reply).toContain(replyString) + for (const replyString of Array.isArray(replyStrings) ? replyStrings : [replyStrings]) { + expect(reply).toContain(replyString) + } expect(reply).toMatchSnapshot(reply) } export async function getReplyToServer(socket: SocketMock, messageId: number): Promise { From 46841160704e11e6ac00bcdee0e3bbf828c54393 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Thu, 1 Feb 2024 09:25:29 +0100 Subject: [PATCH 4/5] fix: roStoryMoveMultiple: handle edge case of single storyID --- packages/connector/src/MosDevice.ts | 49 +++- packages/connector/src/__mocks__/testData.ts | 13 + .../src/__tests__/Profile2-non-strict.spec.ts | 16 ++ .../connector/src/__tests__/Profile2.spec.ts | 16 ++ .../Profile2-non-strict.spec.ts.snap | 40 +++ .../__snapshots__/Profile2.spec.ts.snap | 236 ++++++++++++++++++ 6 files changed, 364 insertions(+), 6 deletions(-) diff --git a/packages/connector/src/MosDevice.ts b/packages/connector/src/MosDevice.ts index 4682abb1..9db68772 100644 --- a/packages/connector/src/MosDevice.ts +++ b/packages/connector/src/MosDevice.ts @@ -406,20 +406,57 @@ export class MosDevice implements IMOSDevice { ? (data.roStoryMoveMultiple.storyID as string[]) : [data.roStoryMoveMultiple.storyID as string] - // An aditional validation checking the length of stories can be added - const l = stories.length + { + // From documentation: + // Validation: Duplicate storyIDs are not permitted with in the storyID list. + // This prevents the move from being ambiguous; if two IDs are the same, it is unclear + // where in the playlist the story with duplicate ID must be placed. + const uniqueStoryIds = new Set() + for (const storyId of stories) { + if (uniqueStoryIds.has(storyId)) + return new MosModel.ROAck( + { + ID: this.mosTypes.mosString128.create(data.roStoryMoveMultiple.roID), + Status: this.mosTypes.mosString128.create( + `Duplicate storyIDs are not permitted with in the storyID list.` + ), + Stories: [], + }, + this.strict + ) + uniqueStoryIds.add(storyId) + } + } + + let target: string + let sources: string[] + + if (stories.length > 1) { + target = stories[stories.length - 1] + sources = stories.slice(0, stories.length - 1) + } else { + if (this.strict) { + // Technically a no-op: + target = stories[0] + sources = [] + } else { + // Handling of edge-case: + // If there is only a single storyId, we assume that the single mentioned story should be moved to the end of the playlist + // (ie that there is supposed to be a second, blank storyId that was just omitted by the sender) - const target = stories[l - 1] - const sources = stories.slice(0, l - 1) + target = '' + sources = stories + } + } data.roElementAction = { roID: data.roStoryMoveMultiple.roID, operation: 'MOVE', element_target: { - storyID: l === 1 ? undefined : target, + storyID: target, }, element_source: { - storyID: l === 1 ? stories : sources, + storyID: sources, }, } } else if (data.roItemInsert) { diff --git a/packages/connector/src/__mocks__/testData.ts b/packages/connector/src/__mocks__/testData.ts index eeef8b9d..2d5205ae 100644 --- a/packages/connector/src/__mocks__/testData.ts +++ b/packages/connector/src/__mocks__/testData.ts @@ -117,6 +117,7 @@ const xmlData = { roStorySwap: `5PM V: BRIDGE COLLAPSE P: PHILLIPS INTERVIEW `, roStoryDelete: `5PM V: BRIDGE COLLAPSE P: PHILLIPS INTERVIEW `, roStoryMoveMultiple: `5PM 2 3 5 6 1 `, + roStoryMoveMultiple_single_storyId: `5PM 2 `, roItemInsert: ` 5PM 2597609 5 30848 Hotel Fire vo M00702 testmos \\\\server\\media\\clip392028cd2320s0d.mxfhttp://server/proxy/clipe.wmv 0 900 310 1 Dormitory Fire vo M00705 testmos 0 800 310 `, roItemReplace: ` 5PM 2597609 5 30848 Hotel Fire vo M00702 testmos\\\\server\\media\\clip392028cd2320s0d.mxfhttp://server/proxy/clipe.wmv 0 900 810 1 Dormitory Fire vo M00705 testmos 0 800 610 `, roItemMoveMultiple: `5PM Barn Fire 2 3 5 6 1 `, @@ -1552,6 +1553,18 @@ const xmlApiData = { mosTypes.mosString128.create('5'), mosTypes.mosString128.create('6'), ], + // Thechnically a no-op, but if reading the docs literally...: + roElementAction_roStoryMoveMultiple_single_storyId_action: literal({ + RunningOrderID: mosTypes.mosString128.create('5PM'), + StoryID: mosTypes.mosString128.create('2'), + }), + roElementAction_roStoryMoveMultiple_single_storyId_stories: [], + // Assuming that the single storyId is the story to be moved: + roElementAction_roStoryMoveMultiple_single_storyId_offspec_action: literal({ + RunningOrderID: mosTypes.mosString128.create('5PM'), + StoryID: mosTypes.mosString128.create(''), + }), + roElementAction_roStoryMoveMultiple_single_storyId_offspec_stories: [mosTypes.mosString128.create('2')], roElementAction_roItemInsert_action: literal({ RunningOrderID: mosTypes.mosString128.create('5PM'), StoryID: mosTypes.mosString128.create('2597609'), diff --git a/packages/connector/src/__tests__/Profile2-non-strict.spec.ts b/packages/connector/src/__tests__/Profile2-non-strict.spec.ts index a076c296..376e3924 100644 --- a/packages/connector/src/__tests__/Profile2-non-strict.spec.ts +++ b/packages/connector/src/__tests__/Profile2-non-strict.spec.ts @@ -277,5 +277,21 @@ describe('Profile 2 - non strict', () => { expect(fixSnapshot(onROMoveStories.mock.calls)).toMatchSnapshot() await checkReplyToServer(serverSocketMockLower, messageId, '') }) + test('roStoryMoveMultiple with single storyId', async () => { + // Fake incoming message on socket: + const messageId = await fakeIncomingMessage( + serverSocketMockLower, + xmlData.roStoryMoveMultiple_single_storyId + ) + expect(onROMoveStories).toHaveBeenCalledTimes(1) + expect(onROMoveStories.mock.calls[0][0]).toEqual( + xmlApiData.roElementAction_roStoryMoveMultiple_single_storyId_offspec_action + ) + expect(onROMoveStories.mock.calls[0][1]).toEqual( + xmlApiData.roElementAction_roStoryMoveMultiple_single_storyId_offspec_stories + ) + expect(fixSnapshot(onROMoveStories.mock.calls)).toMatchSnapshot() + await checkReplyToServer(serverSocketMockLower, messageId, '') + }) }) }) diff --git a/packages/connector/src/__tests__/Profile2.spec.ts b/packages/connector/src/__tests__/Profile2.spec.ts index 2ec3096b..466ebda5 100644 --- a/packages/connector/src/__tests__/Profile2.spec.ts +++ b/packages/connector/src/__tests__/Profile2.spec.ts @@ -1219,6 +1219,22 @@ describe('Profile 2', () => { expect(fixSnapshot(onROMoveStories.mock.calls)).toMatchSnapshot() await checkReplyToServer(serverSocketMockLower, messageId, '') }) + test('roStoryMoveMultiple with single storyId', async () => { + // Fake incoming message on socket: + const messageId = await fakeIncomingMessage( + serverSocketMockLower, + xmlData.roStoryMoveMultiple_single_storyId + ) + expect(onROMoveStories).toHaveBeenCalledTimes(1) + expect(onROMoveStories.mock.calls[0][0]).toEqual( + xmlApiData.roElementAction_roStoryMoveMultiple_single_storyId_action + ) + expect(onROMoveStories.mock.calls[0][1]).toEqual( + xmlApiData.roElementAction_roStoryMoveMultiple_single_storyId_stories + ) + expect(fixSnapshot(onROMoveStories.mock.calls)).toMatchSnapshot() + await checkReplyToServer(serverSocketMockLower, messageId, '') + }) test('roItemInsert', async () => { // Fake incoming message on socket: const messageId = await fakeIncomingMessage(serverSocketMockLower, xmlData.roItemInsert) diff --git a/packages/connector/src/__tests__/__snapshots__/Profile2-non-strict.spec.ts.snap b/packages/connector/src/__tests__/__snapshots__/Profile2-non-strict.spec.ts.snap index 14ce1fc0..2e0af120 100644 --- a/packages/connector/src/__tests__/__snapshots__/Profile2-non-strict.spec.ts.snap +++ b/packages/connector/src/__tests__/__snapshots__/Profile2-non-strict.spec.ts.snap @@ -39,3 +39,43 @@ exports[`Profile 2 - non strict deprecated messages roStoryMove - missing second " `; + +exports[`Profile 2 - non strict deprecated messages roStoryMoveMultiple with single storyId 1`] = ` +[ + [ + { + "RunningOrderID": { + "_mosString128": "5PM", + }, + "StoryID": { + "_mosString128": "", + }, + }, + [ + { + "_mosString128": "2", + }, + ], + ], +] +`; + +exports[`Profile 2 - non strict deprecated messages roStoryMoveMultiple with single storyId: + their.mos.id + our.mos.id + 1634 + + runningOrderId + OK + + 1`] = ` +" + their.mos.id + our.mos.id + 1634 + + runningOrderId + OK + +" +`; diff --git a/packages/connector/src/__tests__/__snapshots__/Profile2.spec.ts.snap b/packages/connector/src/__tests__/__snapshots__/Profile2.spec.ts.snap index 1c4aeafd..c16bded5 100644 --- a/packages/connector/src/__tests__/__snapshots__/Profile2.spec.ts.snap +++ b/packages/connector/src/__tests__/__snapshots__/Profile2.spec.ts.snap @@ -49,6 +49,26 @@ exports[`Profile 2 deprecated messages roItemDelete: " `; +exports[`Profile 2 deprecated messages roItemDelete: + their.mos.id + our.mos.id + 1670 + + runningOrderId + OK + + 1`] = ` +" + their.mos.id + our.mos.id + 1670 + + runningOrderId + OK + +" +`; + exports[`Profile 2 deprecated messages roItemInsert 1`] = ` [ [ @@ -131,6 +151,26 @@ exports[`Profile 2 deprecated messages roItemInsert: " `; +exports[`Profile 2 deprecated messages roItemInsert: + their.mos.id + our.mos.id + 1667 + + runningOrderId + OK + + 1`] = ` +" + their.mos.id + our.mos.id + 1667 + + runningOrderId + OK + +" +`; + exports[`Profile 2 deprecated messages roItemMoveMultiple 1`] = ` [ [ @@ -183,6 +223,26 @@ exports[`Profile 2 deprecated messages roItemMoveMultiple: " `; +exports[`Profile 2 deprecated messages roItemMoveMultiple: + their.mos.id + our.mos.id + 1669 + + runningOrderId + OK + + 1`] = ` +" + their.mos.id + our.mos.id + 1669 + + runningOrderId + OK + +" +`; + exports[`Profile 2 deprecated messages roItemReplace 1`] = ` [ [ @@ -265,6 +325,26 @@ exports[`Profile 2 deprecated messages roItemReplace: " `; +exports[`Profile 2 deprecated messages roItemReplace: + their.mos.id + our.mos.id + 1668 + + runningOrderId + OK + + 1`] = ` +" + their.mos.id + our.mos.id + 1668 + + runningOrderId + OK + +" +`; + exports[`Profile 2 deprecated messages roStoryAppend 1`] = ` [ [ @@ -438,6 +518,26 @@ exports[`Profile 2 deprecated messages roStoryDelete: " `; +exports[`Profile 2 deprecated messages roStoryDelete: + their.mos.id + our.mos.id + 1664 + + runningOrderId + OK + + 1`] = ` +" + their.mos.id + our.mos.id + 1664 + + runningOrderId + OK + +" +`; + exports[`Profile 2 deprecated messages roStoryInsert 1`] = ` [ [ @@ -571,6 +671,26 @@ exports[`Profile 2 deprecated messages roStoryInsert: " `; +exports[`Profile 2 deprecated messages roStoryMove - missing second : + their.mos.id + our.mos.id + 1662 + + 5PM + The second <storyID> tag is missing in <roStoryMove>. + + 1`] = ` +" + their.mos.id + our.mos.id + 1662 + + 5PM + The second <storyID> tag is missing in <roStoryMove>. + +" +`; + exports[`Profile 2 deprecated messages roStoryMove 1`] = ` [ [ @@ -591,6 +711,46 @@ exports[`Profile 2 deprecated messages roStoryMove 1`] = ` ] `; +exports[`Profile 2 deprecated messages roStoryMove with blank 1`] = ` +[ + [ + { + "RunningOrderID": { + "_mosString128": "5PM", + }, + "StoryID": { + "_mosString128": "", + }, + }, + [ + { + "_mosString128": "V: BRIDGE COLLAPSE", + }, + ], + ], +] +`; + +exports[`Profile 2 deprecated messages roStoryMove with blank: + their.mos.id + our.mos.id + 1661 + + runningOrderId + OK + + 1`] = ` +" + their.mos.id + our.mos.id + 1661 + + runningOrderId + OK + +" +`; + exports[`Profile 2 deprecated messages roStoryMove: their.mos.id our.mos.id @@ -640,6 +800,42 @@ exports[`Profile 2 deprecated messages roStoryMoveMultiple 1`] = ` ] `; +exports[`Profile 2 deprecated messages roStoryMoveMultiple with single storyId 1`] = ` +[ + [ + { + "RunningOrderID": { + "_mosString128": "5PM", + }, + "StoryID": { + "_mosString128": "2", + }, + }, + [], + ], +] +`; + +exports[`Profile 2 deprecated messages roStoryMoveMultiple with single storyId: + their.mos.id + our.mos.id + 1666 + + runningOrderId + OK + + 1`] = ` +" + their.mos.id + our.mos.id + 1666 + + runningOrderId + OK + +" +`; + exports[`Profile 2 deprecated messages roStoryMoveMultiple: their.mos.id our.mos.id @@ -660,6 +856,26 @@ exports[`Profile 2 deprecated messages roStoryMoveMultiple: " `; +exports[`Profile 2 deprecated messages roStoryMoveMultiple: + their.mos.id + our.mos.id + 1665 + + runningOrderId + OK + + 1`] = ` +" + their.mos.id + our.mos.id + 1665 + + runningOrderId + OK + +" +`; + exports[`Profile 2 deprecated messages roStoryReplace 1`] = ` [ [ @@ -841,6 +1057,26 @@ exports[`Profile 2 deprecated messages roStorySwap: " `; +exports[`Profile 2 deprecated messages roStorySwap: + their.mos.id + our.mos.id + 1663 + + runningOrderId + OK + + 1`] = ` +" + their.mos.id + our.mos.id + 1663 + + runningOrderId + OK + +" +`; + exports[`Profile 2 onCreateRunningOrder 1`] = ` [ [ From 7529616ea04d7563644d31bfe8feba56fc80af60 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Thu, 1 Feb 2024 12:46:42 +0100 Subject: [PATCH 5/5] chore: update snapshot --- .../__snapshots__/Profile2.spec.ts.snap | 140 ------------------ 1 file changed, 140 deletions(-) diff --git a/packages/connector/src/__tests__/__snapshots__/Profile2.spec.ts.snap b/packages/connector/src/__tests__/__snapshots__/Profile2.spec.ts.snap index c16bded5..fe19e9a1 100644 --- a/packages/connector/src/__tests__/__snapshots__/Profile2.spec.ts.snap +++ b/packages/connector/src/__tests__/__snapshots__/Profile2.spec.ts.snap @@ -29,26 +29,6 @@ exports[`Profile 2 deprecated messages roItemDelete 1`] = ` ] `; -exports[`Profile 2 deprecated messages roItemDelete: - their.mos.id - our.mos.id - 1667 - - runningOrderId - OK - - 1`] = ` -" - their.mos.id - our.mos.id - 1667 - - runningOrderId - OK - -" -`; - exports[`Profile 2 deprecated messages roItemDelete: their.mos.id our.mos.id @@ -131,26 +111,6 @@ exports[`Profile 2 deprecated messages roItemInsert 1`] = ` ] `; -exports[`Profile 2 deprecated messages roItemInsert: - their.mos.id - our.mos.id - 1664 - - runningOrderId - OK - - 1`] = ` -" - their.mos.id - our.mos.id - 1664 - - runningOrderId - OK - -" -`; - exports[`Profile 2 deprecated messages roItemInsert: their.mos.id our.mos.id @@ -203,26 +163,6 @@ exports[`Profile 2 deprecated messages roItemMoveMultiple 1`] = ` ] `; -exports[`Profile 2 deprecated messages roItemMoveMultiple: - their.mos.id - our.mos.id - 1666 - - runningOrderId - OK - - 1`] = ` -" - their.mos.id - our.mos.id - 1666 - - runningOrderId - OK - -" -`; - exports[`Profile 2 deprecated messages roItemMoveMultiple: their.mos.id our.mos.id @@ -305,26 +245,6 @@ exports[`Profile 2 deprecated messages roItemReplace 1`] = ` ] `; -exports[`Profile 2 deprecated messages roItemReplace: - their.mos.id - our.mos.id - 1665 - - runningOrderId - OK - - 1`] = ` -" - their.mos.id - our.mos.id - 1665 - - runningOrderId - OK - -" -`; - exports[`Profile 2 deprecated messages roItemReplace: their.mos.id our.mos.id @@ -498,26 +418,6 @@ exports[`Profile 2 deprecated messages roStoryDelete 1`] = ` ] `; -exports[`Profile 2 deprecated messages roStoryDelete: - their.mos.id - our.mos.id - 1662 - - runningOrderId - OK - - 1`] = ` -" - their.mos.id - our.mos.id - 1662 - - runningOrderId - OK - -" -`; - exports[`Profile 2 deprecated messages roStoryDelete: their.mos.id our.mos.id @@ -836,26 +736,6 @@ exports[`Profile 2 deprecated messages roStoryMoveMultiple with single storyId: " `; -exports[`Profile 2 deprecated messages roStoryMoveMultiple: - their.mos.id - our.mos.id - 1663 - - runningOrderId - OK - - 1`] = ` -" - their.mos.id - our.mos.id - 1663 - - runningOrderId - OK - -" -`; - exports[`Profile 2 deprecated messages roStoryMoveMultiple: their.mos.id our.mos.id @@ -1037,26 +917,6 @@ exports[`Profile 2 deprecated messages roStorySwap 1`] = ` ] `; -exports[`Profile 2 deprecated messages roStorySwap: - their.mos.id - our.mos.id - 1661 - - runningOrderId - OK - - 1`] = ` -" - their.mos.id - our.mos.id - 1661 - - runningOrderId - OK - -" -`; - exports[`Profile 2 deprecated messages roStorySwap: their.mos.id our.mos.id