Skip to content

Commit b767b19

Browse files
committed
Adding compliance requirements, conformance test definitions and conformance test implementation for metadata
1 parent 2596631 commit b767b19

17 files changed

Lines changed: 640 additions & 50 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1616
* Added `getCurrentContextWithMetadata()` to the `Channel` interface and `ContextWithMetadata` type, allowing retrieval of both the current context and its associated `ContextMetadata` from a channel. ([#1728](https://github.com/finos/FDC3/pull/1728))
1717
* Added `ContextMetadata` and `AppProvidableContextMetadata` types, replacing the optional `OriginatingAppMetadata` feature with required metadata support on `ContextHandler` and `IntentHandler` callbacks. Desktop Agents MUST provide `source` and `timestamp` metadata, and MUST forward app-supplied `traceId`, `signature` and `custom` fields, supporting observability and security use cases. ([#1728](https://github.com/finos/FDC3/pull/1728))
1818
* Added `version-check` script and integrated it into the `syncpack` script and Publish To NPM workflow to prevent version mismatches causing incorrect npm dist-tags. ([#1864](https://github.com/finos/FDC3/pull/1864))
19+
* Added Channel Interface Compliance and PrivateChannel Interface Compliance subsections to the Desktop Agent API Standard Compliance section in the API spec, enumerating MUST/SHOULD/MAY requirements for all `Channel` and `PrivateChannel` functions including `getCurrentContextWithMetadata`. ([#1728](https://github.com/finos/FDC3/pull/1728))
1920

2021
### Changed
2122

toolbox/fdc3-conformance/src/mock/channel-command.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ export class Fdc3CommandExecutor {
3333
await channelService.broadcastContextItem(contextType, channel!, config.historyItems ?? 1, config.testId);
3434
break;
3535
}
36+
case commands.broadcastInstrumentWithTraceId: {
37+
await channelService.broadcastContextItemWithMetadata('fdc3.instrument', channel!, config.testId, {
38+
traceId: 'test-trace-123',
39+
});
40+
break;
41+
}
42+
case commands.broadcastInstrumentWithSignatureCustom: {
43+
await channelService.broadcastContextItemWithMetadata('fdc3.instrument', channel!, config.testId, {
44+
signature: 'sig-abc',
45+
custom: { region: 'EMEA' },
46+
});
47+
break;
48+
}
3649
}
3750
}
3851

toolbox/fdc3-conformance/src/mock/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ export const commands = {
99
broadcastInstrumentContext: 'broadcastInstrumentContext',
1010
broadcastContactContext: 'broadcastContactContext',
1111
joinUserChannelOne: 'joinUserChannelOne',
12+
broadcastInstrumentWithTraceId: 'broadcastInstrumentWithTraceId',
13+
broadcastInstrumentWithSignatureCustom: 'broadcastInstrumentWithSignatureCustom',
1214
};

toolbox/fdc3-conformance/src/mock/intent-a.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
11
import { closeWindowOnCompletion, sendContextToTests, validateContext } from './mock-functions';
22
import { wait } from '../utils';
33
import { IntentUtilityContext } from '../context-types';
4-
import { IntentResult, getAgent } from '@finos/fdc3';
4+
import { ContextMetadata, IntentResult, getAgent } from '@finos/fdc3';
55
import { ContextType, ControlContextType, Intent } from '../test/support/intent-support';
66

77
getAgent().then(async fdc3 => {
88
await closeWindowOnCompletion(fdc3);
99

1010
//used in 'Raise Intent Result (void result)' and 'Raise Intent (Ignoring any results)'
11-
fdc3.addIntentListener(Intent.aTestingIntent, async (context: IntentUtilityContext): Promise<IntentResult> => {
12-
validateContext(fdc3, context.type, ContextType.testContextX);
13-
await delayExecution(context.delayBeforeReturn);
14-
15-
const { appMetadata } = await fdc3.getInfo();
16-
17-
await sendContextToTests(fdc3, {
18-
type: ControlContextType.A_TESTING_INTENT_LISTENER_TRIGGERED,
19-
instanceId: appMetadata.instanceId,
20-
});
21-
22-
return;
23-
});
11+
fdc3.addIntentListener(
12+
Intent.aTestingIntent,
13+
async (context: IntentUtilityContext, metadata?: ContextMetadata): Promise<IntentResult> => {
14+
validateContext(fdc3, context.type, ContextType.testContextX);
15+
await delayExecution(context.delayBeforeReturn);
16+
17+
const { appMetadata } = await fdc3.getInfo();
18+
19+
const controlContext: Record<string, unknown> = {
20+
type: ControlContextType.A_TESTING_INTENT_LISTENER_TRIGGERED,
21+
instanceId: appMetadata.instanceId,
22+
};
23+
24+
if (metadata) {
25+
controlContext.contextMetadata = {
26+
source: metadata.source,
27+
timestamp: metadata.timestamp instanceof Date ? metadata.timestamp.toISOString() : String(metadata.timestamp),
28+
traceId: metadata.traceId,
29+
signature: metadata.signature,
30+
custom: metadata.custom,
31+
};
32+
}
33+
34+
await sendContextToTests(fdc3, controlContext as unknown as IntentUtilityContext);
35+
36+
return;
37+
}
38+
);
2439

2540
fdc3.addIntentListener(Intent.sharedTestingIntent1, async (context: IntentUtilityContext): Promise<IntentResult> => {
2641
validateContext(fdc3, context.type, ContextType.testContextY);

toolbox/fdc3-conformance/src/mock/interfaces.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Channel } from '@finos/fdc3';
1+
import { AppProvidableContextMetadata, Channel } from '@finos/fdc3';
22

33
export interface IChannelService {
44
joinRetrievedUserChannel(channelId: string): Promise<Channel>;
@@ -7,6 +7,13 @@ export interface IChannelService {
77

88
broadcastContextItem(contextType: string, channel: Channel, historyItems: number, testId: string): Promise<void>;
99

10+
broadcastContextItemWithMetadata(
11+
contextType: string,
12+
channel: Channel,
13+
testId: string,
14+
metadata: AppProvidableContextMetadata
15+
): Promise<void>;
16+
1017
closeWindowOnCompletion(testId: string): Promise<void>;
1118

1219
notifyTestOnCompletion(testId: string): Promise<void>;

toolbox/fdc3-conformance/src/mock/support/channel-support.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Channel, DesktopAgent } from '@finos/fdc3';
1+
import { AppProvidableContextMetadata, Channel, DesktopAgent } from '@finos/fdc3';
22
import constants from '../../constants';
33
import { AppControlContext } from '../../context-types';
44
import { channelType } from '../constants';
@@ -54,6 +54,24 @@ export class ChannelServiceImpl implements IChannelService {
5454
await this.broadcastContextItem('executionComplete', appControlChannel, 1, testId);
5555
}
5656

57+
async broadcastContextItemWithMetadata(
58+
contextType: string,
59+
channel: Channel,
60+
testId: string,
61+
metadata: AppProvidableContextMetadata
62+
): Promise<void> {
63+
const context: AppControlContext = {
64+
type: contextType,
65+
name: 'History-item-1',
66+
testId,
67+
};
68+
if (channel.type === channelType.app) {
69+
await channel.broadcast(context, metadata);
70+
} else {
71+
await this.fdc3.broadcast(context, metadata);
72+
}
73+
}
74+
5775
//get app/system channel broadcast service
5876
private getBroadcastService(currentChannelType: string): IBroadcastService {
5977
if (currentChannelType === channelType.app) {
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { assert, expect } from 'chai';
2+
import { Context, ContextMetadata, ContextWithMetadata } from '@finos/fdc3';
3+
import { ChannelControlImpl } from '../support/channels-support';
4+
import { ContextMetadataValidator } from '../support/context-metadata-support';
5+
import {
6+
JOIN_AND_BROADCAST,
7+
JOIN_AND_BROADCAST_WITH_TRACE_ID,
8+
JOIN_AND_BROADCAST_WITH_SIGNATURE_CUSTOM,
9+
APP_CHANNEL_AND_BROADCAST,
10+
} from '../support/channel-control';
11+
import constants from '../../constants';
12+
import { wait } from '../../utils';
13+
import { getAgent } from '@finos/fdc3';
14+
import { APIDocumentation } from '../support/apiDocuments';
15+
16+
const documentation = '\r\nDocumentation: ' + APIDocumentation.desktopAgent + '\r\nCause:';
17+
const validator = new ContextMetadataValidator();
18+
19+
export default async () => {
20+
const fdc3 = await getAgent();
21+
const cc = new ChannelControlImpl(fdc3);
22+
23+
return describe('fdc3.contextMetadata', () => {
24+
beforeEach(cc.leaveChannel);
25+
26+
afterEach(async function afterEach() {
27+
await cc.closeMockApp(this.currentTest?.title ?? 'Some-Test-Title');
28+
});
29+
30+
// --- User Channel Tests ---
31+
32+
const ucMetadataBroadcast =
33+
'(3.0-UCContextMetadataOnBroadcast) Should receive ContextMetadata with source and timestamp when context is broadcast on a user channel';
34+
it(ucMetadataBroadcast, async () => {
35+
const errorMessage = `\r\nSteps:\r\n- App A adds fdc3.instrument context listener\r\n- App A joins channel\r\n- App B joins channel and broadcasts fdc3.instrument${documentation}`;
36+
37+
const resolveExecutionCompleteListener = cc.initCompleteListener(ucMetadataBroadcast);
38+
let receivedMetadata: ContextMetadata | undefined;
39+
let receivedContext = false;
40+
41+
const listener = await cc.setupAndValidateListener(
42+
null,
43+
'fdc3.instrument',
44+
'fdc3.instrument',
45+
errorMessage,
46+
(_ctx: Context, metadata?: ContextMetadata) => {
47+
receivedMetadata = metadata;
48+
receivedContext = true;
49+
}
50+
);
51+
52+
const channel = await cc.getNonGlobalUserChannel();
53+
await cc.joinChannel(channel);
54+
await cc.openChannelApp(ucMetadataBroadcast, channel.id, JOIN_AND_BROADCAST);
55+
await resolveExecutionCompleteListener;
56+
57+
try {
58+
if (!receivedContext) {
59+
await wait(constants.ShortWait);
60+
}
61+
assert.isTrue(receivedContext, `No context received!${errorMessage}`);
62+
assert.isDefined(receivedMetadata, `No metadata received with context${errorMessage}`);
63+
validator.validateRequiredFields(receivedMetadata!, 'ChannelsAppId');
64+
} finally {
65+
cc.unsubscribeListeners([listener]);
66+
}
67+
});
68+
69+
const ucMetadataTraceId =
70+
'(3.0-UCContextMetadataTraceId) Should receive app-provided traceId in ContextMetadata on user channel broadcast';
71+
it(ucMetadataTraceId, async () => {
72+
const errorMessage = `\r\nSteps:\r\n- App A adds listener\r\n- App B broadcasts with traceId metadata${documentation}`;
73+
74+
const resolveExecutionCompleteListener = cc.initCompleteListener(ucMetadataTraceId);
75+
let receivedMetadata: ContextMetadata | undefined;
76+
let receivedContext = false;
77+
78+
const listener = await cc.setupAndValidateListener(
79+
null,
80+
'fdc3.instrument',
81+
'fdc3.instrument',
82+
errorMessage,
83+
(_ctx: Context, metadata?: ContextMetadata) => {
84+
receivedMetadata = metadata;
85+
receivedContext = true;
86+
}
87+
);
88+
89+
const channel = await cc.getNonGlobalUserChannel();
90+
await cc.joinChannel(channel);
91+
await cc.openChannelApp(ucMetadataTraceId, channel.id, JOIN_AND_BROADCAST_WITH_TRACE_ID);
92+
await resolveExecutionCompleteListener;
93+
94+
try {
95+
if (!receivedContext) {
96+
await wait(constants.ShortWait);
97+
}
98+
assert.isTrue(receivedContext, `No context received!${errorMessage}`);
99+
assert.isDefined(receivedMetadata, `No metadata received${errorMessage}`);
100+
validator.validateRequiredFields(receivedMetadata!);
101+
validator.validateTraceId(receivedMetadata!, 'test-trace-123');
102+
} finally {
103+
cc.unsubscribeListeners([listener]);
104+
}
105+
});
106+
107+
const ucMetadataSignatureCustom =
108+
'(3.0-UCContextMetadataSignatureCustom) Should receive app-provided signature and custom fields in ContextMetadata on user channel broadcast';
109+
it(ucMetadataSignatureCustom, async () => {
110+
const errorMessage = `\r\nSteps:\r\n- App A adds listener\r\n- App B broadcasts with signature and custom metadata${documentation}`;
111+
112+
const resolveExecutionCompleteListener = cc.initCompleteListener(ucMetadataSignatureCustom);
113+
let receivedMetadata: ContextMetadata | undefined;
114+
let receivedContext = false;
115+
116+
const listener = await cc.setupAndValidateListener(
117+
null,
118+
'fdc3.instrument',
119+
'fdc3.instrument',
120+
errorMessage,
121+
(_ctx: Context, metadata?: ContextMetadata) => {
122+
receivedMetadata = metadata;
123+
receivedContext = true;
124+
}
125+
);
126+
127+
const channel = await cc.getNonGlobalUserChannel();
128+
await cc.joinChannel(channel);
129+
await cc.openChannelApp(ucMetadataSignatureCustom, channel.id, JOIN_AND_BROADCAST_WITH_SIGNATURE_CUSTOM);
130+
await resolveExecutionCompleteListener;
131+
132+
try {
133+
if (!receivedContext) {
134+
await wait(constants.ShortWait);
135+
}
136+
assert.isTrue(receivedContext, `No context received!${errorMessage}`);
137+
assert.isDefined(receivedMetadata, `No metadata received${errorMessage}`);
138+
validator.validateRequiredFields(receivedMetadata!);
139+
validator.validateSignature(receivedMetadata!, 'sig-abc');
140+
validator.validateCustom(receivedMetadata!, 'region', 'EMEA');
141+
} finally {
142+
cc.unsubscribeListeners([listener]);
143+
}
144+
});
145+
146+
// --- App Channel Tests ---
147+
148+
const acMetadataBroadcast =
149+
'(3.0-ACContextMetadataOnBroadcast) Should receive ContextMetadata with source and timestamp when context is broadcast on an app channel';
150+
it(acMetadataBroadcast, async () => {
151+
const errorMessage = `\r\nSteps:\r\n- App A gets app channel and adds listener\r\n- App B gets same channel and broadcasts${documentation}`;
152+
153+
const resolveExecutionCompleteListener = cc.initCompleteListener(acMetadataBroadcast);
154+
const testChannel = await fdc3.getOrCreateChannel('test-channel');
155+
let receivedMetadata: ContextMetadata | undefined;
156+
let receivedContext = false;
157+
158+
const listener = await testChannel.addContextListener(
159+
'fdc3.instrument',
160+
(context: Context, metadata?: ContextMetadata) => {
161+
expect(context.type).to.be.equals('fdc3.instrument', errorMessage);
162+
receivedMetadata = metadata;
163+
receivedContext = true;
164+
}
165+
);
166+
167+
await cc.openChannelApp(acMetadataBroadcast, 'test-channel', APP_CHANNEL_AND_BROADCAST);
168+
await resolveExecutionCompleteListener;
169+
170+
try {
171+
if (!receivedContext) {
172+
await wait(constants.ShortWait);
173+
}
174+
assert.isTrue(receivedContext, `No context received!${errorMessage}`);
175+
assert.isDefined(receivedMetadata, `No metadata received${errorMessage}`);
176+
validator.validateRequiredFields(receivedMetadata!, 'ChannelsAppId');
177+
} finally {
178+
listener.unsubscribe();
179+
}
180+
});
181+
182+
// --- getCurrentContextWithMetadata Tests ---
183+
184+
const acGetCurrentContextWithMetadata =
185+
'(3.0-ACGetCurrentContextWithMetadata) getCurrentContextWithMetadata should return context and metadata from an app channel';
186+
it(acGetCurrentContextWithMetadata, async () => {
187+
const errorMessage = `\r\nSteps:\r\n- App B broadcasts to app channel\r\n- App A calls getCurrentContextWithMetadata${documentation}`;
188+
189+
const resolveExecutionCompleteListener = cc.initCompleteListener(acGetCurrentContextWithMetadata);
190+
const testChannel = await fdc3.getOrCreateChannel('test-channel');
191+
192+
await cc.openChannelApp(acGetCurrentContextWithMetadata, 'test-channel', APP_CHANNEL_AND_BROADCAST);
193+
await resolveExecutionCompleteListener;
194+
195+
// Allow time for the broadcast to be stored
196+
await wait(constants.ShortWait);
197+
198+
const result: ContextWithMetadata | null = await testChannel.getCurrentContextWithMetadata('fdc3.instrument');
199+
200+
assert.isNotNull(result, `getCurrentContextWithMetadata returned null${errorMessage}`);
201+
expect(result!.context.type).to.be.equal('fdc3.instrument');
202+
assert.isDefined(result!.metadata, `metadata was not returned${errorMessage}`);
203+
validator.validateRequiredFields(result!.metadata, 'ChannelsAppId');
204+
});
205+
206+
const acGetCurrentContextWithMetadataNull =
207+
'(3.0-ACGetCurrentContextWithMetadataNull) getCurrentContextWithMetadata should return null on an empty channel';
208+
it(acGetCurrentContextWithMetadataNull, async () => {
209+
const testChannel = await fdc3.getOrCreateChannel('test-channel-empty-' + cc.getRandomId());
210+
const result: ContextWithMetadata | null = await testChannel.getCurrentContextWithMetadata('fdc3.instrument');
211+
assert.isNull(result, 'getCurrentContextWithMetadata should return null on an empty channel');
212+
});
213+
});
214+
};

0 commit comments

Comments
 (0)