Skip to content

Commit 367b429

Browse files
Rohit Agrawalcompulimtdurnford
authored
enable internal support for http path to connect to a bot within DirectLineSpeech channel (#3178)
* [HIGH] Bump to 4.9.1-0 (#3162) * 4.9.1-0 * Update samples * Add 4.9.0 * Redirect 4.8 to 4.8.1 * minimum changes to support HTTP path * dont set conversationId * fix token extraction * fix token renewal condition * working code with directline support * add sample with http support * add integration tests for internal http support * fix src of index-http.html * Revert "[HIGH] Bump to 4.9.1-0 (#3162)" This reverts commit 27eaf2b. * fix refresh token logic * fix eslint issues * fix more eslint issues * PR Feedback * fix build issues * fix test failures * fix tests * PR Feedback * remove sample * PR feedback * Fix credentials in test * Typo * Clean up * Add refresh token tests * fix token renewal for directline * disable test or useInternalHttpSupport when speak field si empty Co-authored-by: William Wong <[email protected]> Co-authored-by: TJ Durnford <[email protected]> Co-authored-by: William Wong <[email protected]>
1 parent ed452fb commit 367b429

12 files changed

+354
-109
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import 'global-agent/bootstrap';
6+
7+
import { PropertyId } from 'microsoft-cognitiveservices-speech-sdk';
8+
import { timeouts } from './constants.json';
9+
import createTestHarness from './utilities/createTestHarness';
10+
import MockAudioContext from './utilities/MockAudioContext';
11+
12+
jest.setTimeout(timeouts.test);
13+
14+
beforeEach(() => {
15+
global.AudioContext = MockAudioContext;
16+
});
17+
18+
const realSetTimeout = setTimeout;
19+
20+
function sleep(intervalMS) {
21+
return new Promise(resolve => realSetTimeout(resolve, intervalMS));
22+
}
23+
24+
async function waitUntil(fn, timeout = 5000, intervalMS = 1000) {
25+
const startTime = Date.now();
26+
27+
while (Date.now() - startTime < timeout) {
28+
if (fn()) {
29+
return;
30+
}
31+
32+
await sleep(intervalMS);
33+
}
34+
35+
throw new Error('timed out');
36+
}
37+
38+
test('should refresh authorization token', async () => {
39+
jest.useFakeTimers('modern');
40+
41+
const { directLine } = await createTestHarness();
42+
const initialAuthorizationToken = directLine.dialogServiceConnector.authorizationToken;
43+
44+
// Wait until 2 seconds in real-time clock, to make sure the token renewed is different (JWT has a per-second timestamp).
45+
await sleep(2000);
46+
47+
// Fast-forward 15 minutes to kick-off the token renewal
48+
jest.advanceTimersByTime(120000);
49+
50+
// Wait for 5 seconds until the token get updated
51+
await waitUntil(() => initialAuthorizationToken !== directLine.dialogServiceConnector.authorizationToken);
52+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import 'global-agent/bootstrap';
6+
7+
import { PropertyId } from 'microsoft-cognitiveservices-speech-sdk';
8+
import { timeouts } from './constants.json';
9+
import createTestHarness from './utilities/createTestHarness';
10+
import MockAudioContext from './utilities/MockAudioContext';
11+
12+
jest.setTimeout(timeouts.test);
13+
14+
beforeEach(() => {
15+
global.AudioContext = MockAudioContext;
16+
});
17+
18+
const realSetTimeout = setTimeout;
19+
20+
function sleep(intervalMS) {
21+
return new Promise(resolve => realSetTimeout(resolve, intervalMS));
22+
}
23+
24+
async function waitUntil(fn, timeout = 5000, intervalMS = 1000) {
25+
const startTime = Date.now();
26+
27+
while (Date.now() - startTime < timeout) {
28+
if (fn()) {
29+
return;
30+
}
31+
32+
await sleep(intervalMS);
33+
}
34+
35+
throw new Error('timed out');
36+
}
37+
38+
test('should refresh Direct Line token', async () => {
39+
jest.useFakeTimers('modern');
40+
41+
const { directLine } = await createTestHarness({ enableInternalHTTPSupport: true });
42+
const initialToken = directLine.dialogServiceConnector.properties.getProperty(PropertyId.Conversation_ApplicationId);
43+
44+
// Wait until 2 seconds in real-time clock, to make sure the token renewed is different (JWT has a per-second timestamp).
45+
await sleep(2000);
46+
47+
// Fast-forward 15 minutes to kick-off the token renewal
48+
jest.advanceTimersByTime(900000);
49+
50+
// Wait for 5 seconds until the token get updated
51+
await waitUntil(
52+
() =>
53+
initialToken !==
54+
directLine.dialogServiceConnector.properties.getProperty(PropertyId.Conversation_ApplicationId, 5000)
55+
);
56+
});

packages/directlinespeech/__tests__/sendSpeechActivity.js

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,51 +18,54 @@ beforeEach(() => {
1818
global.AudioContext = MockAudioContext;
1919
});
2020

21-
test('should echo back when saying "hello" and "world"', async () => {
22-
const { directLine, sendTextAsSpeech } = await createTestHarness();
21+
describe.each([['without internal HTTP support'], ['with internal HTTP support', { enableInternalHTTPSupport: true }]])(
22+
'%s',
23+
(_, testHarnessOptions) => {
24+
test('should echo back when saying "hello" and "world"', async () => {
25+
const { directLine, fetchCredentials, sendTextAsSpeech } = await createTestHarness(testHarnessOptions);
2326

24-
const connectedPromise = waitForConnected(directLine);
25-
const activitiesPromise = subscribeAll(take(directLine.activity$, 2));
27+
const connectedPromise = waitForConnected(directLine);
28+
const activitiesPromise = subscribeAll(take(directLine.activity$, 2));
2629

27-
await connectedPromise;
30+
await connectedPromise;
2831

29-
await sendTextAsSpeech('hello');
30-
await sendTextAsSpeech('world');
32+
await sendTextAsSpeech('hello');
33+
await sendTextAsSpeech('world');
3134

32-
const activities = await activitiesPromise;
33-
const activityUtterances = Promise.all(activities.map(activity => recognizeActivityAsText(activity)));
34-
35-
await expect(activityUtterances).resolves.toMatchInlineSnapshot(`
36-
Array [
37-
"Hello.",
38-
"World.",
39-
]
40-
`);
41-
});
35+
const activities = await activitiesPromise;
36+
const activityUtterances = Promise.all(
37+
activities.map(activity => recognizeActivityAsText(activity, { fetchCredentials }))
38+
);
4239

43-
test('should echo back "Bellevue" when saying "bellview"', async () => {
44-
const { directLine, sendTextAsSpeech } = await createTestHarness();
40+
await expect(activityUtterances).resolves.toEqual(['Hello.', 'World.']);
41+
});
4542

46-
const connectedPromise = waitForConnected(directLine);
47-
const activitiesPromise = subscribeAll(take(directLine.activity$, 1));
43+
test('should echo back "Bellevue" when saying "bellview"', async () => {
44+
const { directLine, fetchCredentials, sendTextAsSpeech } = await createTestHarness(testHarnessOptions);
4845

49-
await connectedPromise;
46+
const connectedPromise = waitForConnected(directLine);
47+
const activitiesPromise = subscribeAll(take(directLine.activity$, 1));
5048

51-
await sendTextAsSpeech('bellview');
49+
await connectedPromise;
5250

53-
const activities = await activitiesPromise;
54-
const activityUtterances = Promise.all(activities.map(activity => recognizeActivityAsText(activity)));
51+
await sendTextAsSpeech('bellview');
5552

56-
await expect(activityUtterances).resolves.toMatchInlineSnapshot(`
57-
Array [
58-
"Bellevue.",
59-
]
60-
`);
61-
});
53+
const activities = await activitiesPromise;
54+
const activityUtterances = Promise.all(
55+
activities.map(activity => recognizeActivityAsText(activity, { fetchCredentials }))
56+
);
57+
58+
await expect(activityUtterances).resolves.toEqual(['Bellevue.']);
59+
});
60+
61+
62+
}
63+
);
6264

65+
// TODO: Re-enable this test for "enableInternalHttpSupport = true" once DLS bug fix is lit up in production.
6366
// 2020-05-11: Direct Line Speech protocol was updated to synthesize "text" if "speak" property is not set.
6467
test('should synthesis if "speak" is empty', async () => {
65-
const { directLine, sendTextAsSpeech } = await createTestHarness();
68+
const { directLine, fetchCredentials, sendTextAsSpeech } = await createTestHarness();
6669

6770
const connectedPromise = waitForConnected(directLine);
6871
const activitiesPromise = subscribeAll(take(directLine.activity$, 1));
@@ -73,7 +76,10 @@ test('should synthesis if "speak" is empty', async () => {
7376
await sendTextAsSpeech("Don't speak anything.");
7477

7578
const activities = await activitiesPromise;
76-
const activityUtterances = await Promise.all(activities.map(activity => recognizeActivityAsText(activity)));
79+
const activityUtterances = await Promise.all(
80+
activities.map(activity => recognizeActivityAsText(activity, { fetchCredentials }))
81+
);
7782

83+
// Despite it does not have "speak" property, Direct Line Speech protocol will fallback to "text" property for synthesize.
7884
expect(activityUtterances).toEqual([`Don't speak anything.`]);
7985
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import fetch from 'node-fetch';
2+
3+
const TOKEN_URL_TEMPLATE = 'https://{region}.api.cognitive.microsoft.com/sts/v1.0/issueToken';
4+
5+
async function fetchBaseSpeechCredentialsFromWaterBottle() {
6+
const res = await fetch('https://webchat-waterbottle.azurewebsites.net/token/speechservices');
7+
8+
if (!res.ok) {
9+
throw new Error(`Failed to fetch Cognitive Services Speech Services credentials, server returned ${res.status}`);
10+
}
11+
12+
const { region, token: authorizationToken } = await res.json();
13+
14+
return { authorizationToken, region };
15+
}
16+
17+
async function fetchBaseSpeechCredentialsFromSubscriptionKey({ region, subscriptionKey }) {
18+
const res = await fetch(TOKEN_URL_TEMPLATE.replace(/\{region\}/u, region), {
19+
headers: {
20+
'Ocp-Apim-Subscription-Key': subscriptionKey
21+
},
22+
method: 'POST'
23+
});
24+
25+
if (!res.ok) {
26+
throw new Error(`Failed to fetch authorization token, server returned ${res.status}`);
27+
}
28+
29+
return {
30+
authorizationToken: await res.text(),
31+
region
32+
};
33+
}
34+
35+
async function fetchDirectLineTokenFromWaterBottle() {
36+
const directLineTokenResult = await fetch('https://webchat-waterbottle.azurewebsites.net/token/directline');
37+
38+
if (!directLineTokenResult.ok) {
39+
throw new Error(
40+
`Failed to fetch Cognitive Services Direct Line credentials, server returned ${directLineTokenResult.status}`
41+
);
42+
}
43+
44+
const { token: directLineToken } = await directLineTokenResult.json();
45+
46+
return { directLineToken };
47+
}
48+
49+
async function fetchDirectLineCredentialsFromDirectLineSecret(channelSecret) {
50+
const res = await fetch('https://directline.botframework.com/v3/directline/tokens/generate', {
51+
headers: {
52+
Authorization: `Bearer ${channelSecret}`
53+
},
54+
method: 'POST'
55+
});
56+
57+
if (!res.ok) {
58+
throw new Error(`Failed to fetch authorization token for Direct Line, server returned ${res.status}`);
59+
}
60+
61+
const { token } = await res.json();
62+
63+
return { directLineToken };
64+
}
65+
66+
export default function createFetchCredentials({ enableInternalHTTPSupport } = {}) {
67+
let cachedCredentials;
68+
69+
setInterval(() => {
70+
cachedCredentials = null;
71+
}, 120000);
72+
73+
return () => {
74+
if (!cachedCredentials) {
75+
const {
76+
SPEECH_SERVICES_DIRECT_LINE_SECRET,
77+
SPEECH_SERVICES_REGION,
78+
SPEECH_SERVICES_SUBSCRIPTION_KEY
79+
} = process.env;
80+
81+
let baseCredentialsPromise;
82+
let additionalCredentialsPromise;
83+
84+
if (SPEECH_SERVICES_REGION && SPEECH_SERVICES_SUBSCRIPTION_KEY) {
85+
baseCredentialsPromise = fetchBaseSpeechCredentialsFromSubscriptionKey({
86+
region: SPEECH_SERVICES_REGION,
87+
subscriptionKey: SPEECH_SERVICES_SUBSCRIPTION_KEY
88+
});
89+
90+
if (enableInternalHTTPSupport) {
91+
if (!SPEECH_SERVICES_DIRECT_LINE_SECRET) {
92+
throw new Error(
93+
`Failed to fetch Direct Line token as SPEECH_SERVICES_DIRECT_LINE_SECRET environment variable is not set`
94+
);
95+
}
96+
97+
additionalCredentialsPromise = fetchDirectLineCredentialsFromDirectLineSecret(
98+
SPEECH_SERVICES_DIRECT_LINE_SECRET
99+
);
100+
}
101+
} else {
102+
baseCredentialsPromise = fetchBaseSpeechCredentialsFromWaterBottle();
103+
104+
if (enableInternalHTTPSupport) {
105+
additionalCredentialsPromise = fetchDirectLineTokenFromWaterBottle();
106+
}
107+
}
108+
109+
cachedCredentials = (async () => ({
110+
...(await baseCredentialsPromise),
111+
...(await (additionalCredentialsPromise || {}))
112+
}))();
113+
}
114+
115+
return cachedCredentials;
116+
};
117+
}

packages/directlinespeech/__tests__/utilities/createTestHarness.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import createDeferred from 'p-defer-es5';
22

33
import createAdapters from '../../src/createAdapters';
4+
import createFetchCredentials from './createFetchCredentials';
45
import createQueuedArrayBufferAudioSource from './createQueuedArrayBufferAudioSource';
5-
import fetchSpeechCredentialsWithCache from './fetchSpeechCredentialsWithCache';
66
import fetchSpeechData from './fetchSpeechData';
77

8-
export default async function createTestHarness() {
8+
export default async function createTestHarness({ enableInternalHTTPSupport } = {}) {
99
const audioConfig = createQueuedArrayBufferAudioSource();
10+
const fetchCredentials = createFetchCredentials({ enableInternalHTTPSupport });
11+
1012
const { directLine, webSpeechPonyfillFactory } = await createAdapters({
1113
audioConfig,
12-
fetchCredentials: fetchSpeechCredentialsWithCache
14+
fetchCredentials,
15+
enableInternalHTTPSupport
1316
});
1417

1518
return {
1619
directLine,
20+
fetchCredentials,
1721
sendTextAsSpeech: async text => {
18-
audioConfig.push(await fetchSpeechData({ text }));
22+
audioConfig.push(await fetchSpeechData({ fetchCredentials, text }));
1923

2024
// Create a new SpeechRecognition session and start it.
2125
// By SpeechRecognition.start(), it will invoke Speech SDK to start grabbing speech data from AudioConfig.

0 commit comments

Comments
 (0)