Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- [client] Fixed an issue with the transcripts path input inside of the resource settings dialog in PR [1836](https://github.com/microsoft/BotFramework-Emulator/pull/1836)
- [client] Implemented HTML app menu for Windows in PR [1893](https://github.com/microsoft/BotFramework-Emulator/pull/1893)
- [client/main] Migrated from Bing Speech API to Cognitive Services Speech API in PR [1878](https://github.com/microsoft/BotFramework-Emulator/pull/1878)


## v4.5.2 - 2019 - 07 - 17
Expand Down
2 changes: 1 addition & 1 deletion packages/app/client/src/state/sagas/chatSagas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jest.mock('electron', () => {

jest.mock('botframework-webchat', () => {
return {
createCognitiveServicesBingSpeechPonyfillFactory: () => () => 'Yay! ponyfill!',
createCognitiveServicesSpeechServicesPonyfillFactory: () => () => 'Yay! ponyfill!',
};
});

Expand Down
21 changes: 11 additions & 10 deletions packages/app/client/src/state/sagas/chatSagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { Activity } from 'botframework-schema';
import { SharedConstants, ValueTypes, newNotification } from '@bfemulator/app-shared';
import { CommandServiceImpl, CommandServiceInstance, ConversationService } from '@bfemulator/sdk-shared';
import { IEndpointService } from 'botframework-config/lib/schema';
import { createCognitiveServicesBingSpeechPonyfillFactory } from 'botframework-webchat';
import { createCognitiveServicesSpeechServicesPonyfillFactory } from 'botframework-webchat';
import { createStore as createWebChatStore } from 'botframework-webchat-core';
import { call, ForkEffect, put, select, takeEvery, takeLatest } from 'redux-saga/effects';

Expand Down Expand Up @@ -157,23 +157,24 @@ export class ChatSagas {
// If an existing factory is found, refresh the token
const existingFactory: string = yield select(getWebSpeechFactoryForDocumentId, documentId);
const { GetSpeechToken: command } = SharedConstants.Commands.Emulator;
let token;

try {
token = yield call(
[ChatSagas.commandService, ChatSagas.commandService.remoteCall],
const speechAuthenticationToken: Promise<string> = ChatSagas.commandService.remoteCall(
command,
endpoint.id,
!!existingFactory
);
} catch (e) {
// No-op - this appId/pass combo is not provisioned to use the speech api
}
if (token) {
const factory = yield call(createCognitiveServicesBingSpeechPonyfillFactory, {
authorizationToken: token,

const factory = yield call(createCognitiveServicesSpeechServicesPonyfillFactory, {
authorizationToken: speechAuthenticationToken,
region: 'westus', // Currently, the prod speech service is only deployed to westus
});

yield put(webSpeechFactoryUpdated(documentId, factory)); // Provide the new factory to the store
} catch (e) {
// No-op - this appId/pass combo is not provisioned to use the speech api
Comment thread
tonyanziano marked this conversation as resolved.
}

yield put(updatePendingSpeechTokenRetrieval(false));
if (resolver) {
resolver();
Expand Down
1 change: 0 additions & 1 deletion packages/app/shared/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,5 @@ export * from './fileTypes';
export * from './notificationTypes';
export * from './responseTypes';
export * from './serverSettingsTypes';
export * from './speechTypes';
export * from './luisTypes';
export * from './clientAwareSettings';
4 changes: 2 additions & 2 deletions packages/emulator/core/src/authEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ export const v32Authentication = {
};

export const speech = {
// Access token for Bing Speech Api
tokenEndpoint: 'https://login.botframework.com/v3/speechtoken',
// Access token for Cognitive Services API
tokenEndpoint: 'https://login.botframework.com/v3/speechtoken/speechservices',
};
176 changes: 121 additions & 55 deletions packages/emulator/core/src/facility/botEndpoint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,92 +38,157 @@ import { authentication, usGovernmentAuthentication } from '../authEndpoints';
import BotEndpoint from './botEndpoint';

describe('BotEndpoint', () => {
it('should return the speech token if it already exists', async () => {
it('should determine whether a token will expire within a time period', () => {
const endpoint = new BotEndpoint();
endpoint.speechToken = 'someToken';
const currentTime = Date.now();
endpoint.speechAuthenticationToken = {
expireAt: currentTime + 100, // 100 ms in the future
} as any;

expect((endpoint as any).willTokenExpireWithin(5000)).toBe(true);
});

it('should return the speech token if it already exists', async () => {
const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw');
endpoint.speechAuthenticationToken = {
accessToken: 'someToken',
region: 'westus2',
expireAt: Date.now() + 10 * 1000 * 60, // expires in 10 minutes
tokenLife: 10 * 1000 * 60, // token life of 10 minutes
};
const refresh = false;
const token = await endpoint.getSpeechToken(refresh);
expect(token).toBe('someToken');
});

it('should return a new speech token if the current token is expired', async () => {
const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw');
endpoint.speechAuthenticationToken = {
expireAt: Date.now() - 5000,
} as any;
jest.spyOn(endpoint as any, 'fetchSpeechToken').mockResolvedValueOnce('new speech token');
const token = await endpoint.getSpeechToken();

expect(token).toBe('new speech token');
});

it('should return a new speech token if the current token is past its half life', async () => {
const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw');
endpoint.speechAuthenticationToken = {
expireAt: Date.now() + 4 * 1000 * 60, // expires in 4 minutes
tokenLife: 10 * 1000 * 60, // token life of 10 minutes
} as any;
jest.spyOn(endpoint as any, 'fetchSpeechToken').mockResolvedValueOnce('new speech token');
const token = await endpoint.getSpeechToken();

expect(token).toBe('new speech token');
});

it('should return a new speech token if there is no existing token or if the refresh flag is true', async () => {
const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw');
jest.spyOn(endpoint as any, 'fetchSpeechToken').mockResolvedValueOnce('new speech token');
const token = await endpoint.getSpeechToken(true);

expect(token).toBe('new speech token');
});

it('should throw if there is no msa app id or password', async () => {
const endpoint = new BotEndpoint();
try {
await endpoint.getSpeechToken();
} catch (e) {
expect(e).toEqual(new Error('bot must have Microsoft App ID and password'));
expect(e).toEqual(new Error('Bot must have a valid Microsoft App ID and password'));
}
});

it('should return a speech token', async () => {
/* eslint-disable typescript/camelcase */
const mockResponse = {
json: async () => Promise.resolve({ access_Token: 'someSpeechToken' }),
it('should fetch a speech token', async () => {
const endpoint = new BotEndpoint();
jest.spyOn(endpoint as any, 'fetchWithAuth').mockResolvedValueOnce({
json: () =>
Promise.resolve({
// eslint-disable-next-line typescript/camelcase
access_Token: 'someSpeechToken',
region: 'westus2',
expireAt: 1234,
tokenLife: 9999,
}),
status: 200,
};
const mockFetchWithAuth = jest.fn(() => Promise.resolve(mockResponse));
const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw');
endpoint.fetchWithAuth = mockFetchWithAuth;
const token = await endpoint.getSpeechToken();
});
const token = await (endpoint as any).fetchSpeechToken();

expect(token).toBe('someSpeechToken');
});

it('should throw if there is no access_Token in the response', async () => {
/* eslint-disable typescript/camelcase */
it('should throw when failing to read the token response', async () => {
const endpoint = new BotEndpoint();
jest.spyOn(endpoint as any, 'fetchWithAuth').mockResolvedValueOnce({
json: async () => Promise.reject(new Error('Malformed response JSON.')),
status: 200,
});

// with error in response body
let mockResponse: any = {
json: async () => Promise.resolve({ error: 'someError' }),
try {
await (endpoint as any).fetchSpeechToken();
expect(false).toBe(true); // make sure catch is hit
} catch (e) {
expect(e).toEqual(new Error(`Couldn't read speech token response: ${new Error('Malformed response JSON.')}`));
}
});

it(`should throw when the token response doesn't contain a token and has an error attached`, async () => {
const endpoint = new BotEndpoint();
jest.spyOn(endpoint as any, 'fetchWithAuth').mockResolvedValueOnce({
json: () => Promise.resolve({ error: 'Token was lost in transit.' }),
status: 200,
};
const mockFetchWithAuth = jest.fn(() => Promise.resolve(mockResponse));
const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw');
endpoint.fetchWithAuth = mockFetchWithAuth;
});

try {
await endpoint.getSpeechToken();
await (endpoint as any).fetchSpeechToken();
expect(false).toBe(true); // make sure catch is hit
} catch (e) {
expect(e).toEqual(new Error('someError'));
expect(e).toEqual(new Error('Token was lost in transit.'));
}
});

// with no error in response body
mockResponse = {
json: async () => Promise.resolve({}),
it(`should throw when the token response doesn't contain a token nor an error`, async () => {
const endpoint = new BotEndpoint();
jest.spyOn(endpoint as any, 'fetchWithAuth').mockResolvedValueOnce({
json: () => Promise.resolve({}),
status: 200,
};
});

try {
await endpoint.getSpeechToken();
await (endpoint as any).fetchSpeechToken();
expect(false).toBe(true); // make sure catch is hit
} catch (e) {
expect(e).toEqual(new Error('could not retrieve speech token'));
expect(e).toEqual(new Error('Could not retrieve speech token'));
}
});

it('should throw if the call to the speech service returns a 401', async () => {
/* eslint-disable typescript/camelcase */
const mockResponse: any = {
it(`should throw when the token endpoint returns a 401`, async () => {
const endpoint = new BotEndpoint();
jest.spyOn(endpoint as any, 'fetchWithAuth').mockResolvedValueOnce({
status: 401,
};
const mockFetchWithAuth = jest.fn(() => Promise.resolve(mockResponse));
const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw');
endpoint.fetchWithAuth = mockFetchWithAuth;
});

try {
await endpoint.getSpeechToken();
await (endpoint as any).fetchSpeechToken();
expect(false).toBe(true); // make sure catch is hit
} catch (e) {
expect(e).toEqual(new Error('not authorized to use Cognitive Services Speech API'));
expect(e).toEqual(new Error('Not authorized to use Cognitive Services Speech API'));
}
});

it('should throw if the call to the speech service returns a non-200', async () => {
/* eslint-disable typescript/camelcase */
const mockResponse: any = {
it(`should throw when the token endpoint returns an error response that is not a 401`, async () => {
const endpoint = new BotEndpoint();
jest.spyOn(endpoint as any, 'fetchWithAuth').mockResolvedValueOnce({
status: 500,
};
const mockFetchWithAuth = jest.fn(() => Promise.resolve(mockResponse));
const endpoint = new BotEndpoint('', '', '', 'msaAppId', 'msaAppPw');
endpoint.fetchWithAuth = mockFetchWithAuth;
});

try {
await endpoint.getSpeechToken();
await (endpoint as any).fetchSpeechToken();
expect(false).toBe(true); // make sure catch is hit
} catch (e) {
expect(e).toEqual(new Error('cannot retrieve speech token'));
expect(e).toEqual(new Error(`Can't retrieve speech token`));
}
});

Expand Down Expand Up @@ -191,7 +256,7 @@ describe('BotEndpoint', () => {
const accessTokenExpires = Date.now() * 2 + tokenRefreshTime;
endpoint.accessTokenExpires = accessTokenExpires;
// using non-v1.0 token & standard endpoint
const mockOauthResponse = { access_token: 'I am an access token!', expires_in: 10 };
const mockOauthResponse = { access_token: 'I am an access token!', expires_in: 10 }; // eslint-disable-line typescript/camelcase
const mockResponse = { json: jest.fn(() => Promise.resolve(mockOauthResponse)), status: 200 };
const mockFetch = jest.fn(() => Promise.resolve(mockResponse));
(endpoint as any)._options = { fetch: mockFetch };
Expand All @@ -204,9 +269,9 @@ describe('BotEndpoint', () => {
expect(mockFetch).toHaveBeenCalledWith(authentication.tokenEndpoint, {
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: msaAppId,
client_secret: msaPw,
grant_type: 'client_credentials', // eslint-disable-line typescript/camelcase
client_id: msaAppId, // eslint-disable-line typescript/camelcase
client_secret: msaPw, // eslint-disable-line typescript/camelcase
scope: `${msaAppId}/.default`,
} as { [key: string]: string }).toString(),
headers: {
Expand All @@ -226,9 +291,9 @@ describe('BotEndpoint', () => {
expect(mockFetch).toHaveBeenCalledWith(usGovernmentAuthentication.tokenEndpoint, {
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: msaAppId,
client_secret: msaPw,
grant_type: 'client_credentials', // eslint-disable-line typescript/camelcase
client_id: msaAppId, // eslint-disable-line typescript/camelcase
client_secret: msaPw, // eslint-disable-line typescript/camelcase
scope: `${msaAppId}/.default`,
atver: '1',
} as { [key: string]: string }).toString(),
Expand All @@ -255,7 +320,8 @@ describe('BotEndpoint', () => {
(endpoint as any)._options = { fetch: mockFetch };

try {
const response = await (endpoint as any).getAccessToken();
await (endpoint as any).getAccessToken();
expect(false).toBe(true); // make sure catch is hit
} catch (e) {
expect(e).toEqual({ body: undefined, message: 'Refresh access token failed with status code: 404', status: 404 });
}
Expand Down
Loading