Skip to content
This repository has been archived by the owner on Aug 14, 2021. It is now read-only.

Commit

Permalink
feat: API key integration (CORE-5134) (#37)
Browse files Browse the repository at this point in the history
* chore: add verror to project

* feat: add API key to authorization header of interact requests

* feat: add apiKey param to RuntimeClientFactory

* test: fix current unit tests

* test: add new unit tests

* chore: run lint

* chore: remove extra space

* fix: move API key to Client instead of RuntimeClient and pass into axios.create()

* test: fix unit tests

* fix: re-order imports
  • Loading branch information
jgoping authored Feb 25, 2021
1 parent 2b64aa4 commit 82f3fa5
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 14 deletions.
15 changes: 12 additions & 3 deletions lib/Client/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { State } from '@voiceflow/runtime';
import VError from '@voiceflow/verror';
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import _cloneDeep from 'lodash/cloneDeep';

import { RequestContext, ResponseContext } from '@/lib/types';

import { adaptResponseContext, extractAudioStep } from './adapters';

export type ClientConfig<S> = { variables?: Partial<S>; endpoint: string; versionID: string; axiosConfig?: AxiosRequestConfig };
export type ClientConfig<S> = { variables?: Partial<S>; endpoint: string; versionID: string; apiKey: string; axiosConfig?: AxiosRequestConfig };

export class Client<S extends Record<string, any> = Record<string, any>> {
private axios: AxiosInstance;
Expand All @@ -17,8 +18,12 @@ export class Client<S extends Record<string, any> = Record<string, any>> {

private initVariables: Partial<S> | undefined;

constructor({ variables, endpoint, versionID, axiosConfig }: ClientConfig<S>) {
this.axios = axios.create({ ...axiosConfig, baseURL: endpoint });
constructor({ variables, endpoint, versionID, apiKey, axiosConfig }: ClientConfig<S>) {
if (!Client.isAPIKey(apiKey)) {
throw new VError('Invalid API key', VError.HTTP_STATUS.UNAUTHORIZED);
}

this.axios = axios.create({ ...axiosConfig, baseURL: endpoint, headers: { authorization: apiKey } });

this.initVariables = variables;
this.versionID = versionID;
Expand Down Expand Up @@ -49,6 +54,10 @@ export class Client<S extends Record<string, any> = Record<string, any>> {
getVersionID() {
return this.versionID;
}

static isAPIKey(authorization?: string): boolean {
return !!authorization && authorization.startsWith('VF.') && authorization.match(/\./g)!.length === 2;
}
}

export default Client;
5 changes: 3 additions & 2 deletions lib/RuntimeClientFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DEFAULT_ENDPOINT } from './constants';

export type FactoryConfig<S extends State['variables']> = {
versionID: string;
apiKey: string;
endpoint?: string;
dataConfig?: DataConfig;
variables?: Partial<S>;
Expand All @@ -23,12 +24,12 @@ export class RuntimeClientFactory<S extends Record<string, any> = Record<string,

private defaultState: State;

constructor({ versionID, endpoint = DEFAULT_ENDPOINT, dataConfig, variables, axiosConfig }: FactoryConfig<S>) {
constructor({ versionID, endpoint = DEFAULT_ENDPOINT, apiKey, dataConfig, variables, axiosConfig }: FactoryConfig<S>) {
if (variables) {
validateVarMerge(variables);
}

this.client = new Client({ variables, endpoint, versionID, axiosConfig });
this.client = new Client({ variables, endpoint, versionID, apiKey, axiosConfig });
this.defaultState = { stack: [], storage: {}, variables: { ...variables } };

this.dataConfig = {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@types/lodash": "^4.14.168",
"@voiceflow/general-types": "^1.29.0",
"@voiceflow/runtime": "^1.20.1",
"@voiceflow/verror": "^1.1.0",
"axios": "^0.21.1",
"bluebird": "^3.7.2",
"html-parse-stringify": "^2.0.0",
Expand Down
17 changes: 14 additions & 3 deletions tests/lib/Client/index.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Client, { ClientConfig } from '@/lib/Client';
import { DEFAULT_ENDPOINT } from '@/lib/RuntimeClientFactory/constants';

import { SEND_TEXT_REQUEST_BODY, SEND_TEXT_RESPONSE_BODY, VERSION_ID, VF_APP_INITIAL_STATE } from '../Context/fixtures';
import { INTERACT_ENDPOINT, STATE_ENDPOINT } from '../fixtures';
import { API_KEY, INTERACT_ENDPOINT, STATE_ENDPOINT } from '../fixtures';
import { VF_APP_CUSTOM_INITIAL_VARIABLES } from './fixtures';

chai.use(chaiAsPromise);
Expand All @@ -27,7 +27,7 @@ const createClient = <S = any>(config?: Partial<ClientConfig<S>>) => {

const axiosCreate = sinon.stub(baseAxios, 'create').returns(axiosInstance as any);

const client = new Client({ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, ...(config as any) });
const client = new Client({ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, apiKey: API_KEY, ...(config as any) });

return { client, axiosCreate, axiosInstance };
};
Expand All @@ -45,23 +45,34 @@ describe('Client', () => {
expect(axiosCreate.args[0]).to.eql([
{
baseURL: DEFAULT_ENDPOINT,
headers: { authorization: API_KEY },
},
]);
});

it('options', () => {
const versionID = 'customVersionID';
const endpoint = 'customEndpoint';
const { axiosCreate, client } = createClient({ versionID, endpoint });
const apiKey = 'VF.custom.apiKey';
const { axiosCreate, client } = createClient({ versionID, endpoint, apiKey });

expect(axiosCreate.callCount).to.eql(1);
expect(axiosCreate.args[0]).to.eql([
{
baseURL: endpoint,
headers: { authorization: apiKey },
},
]);
expect(client.getVersionID()).to.eql(versionID);
});

it('invalid API key', () => {
expect(() => new Client({ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, apiKey: undefined as any })).to.throw('Invalid API key');
expect(() => new Client({ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, apiKey: 'hello' as any })).to.throw('Invalid API key');
expect(() => new Client({ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, apiKey: 'VF.' as any })).to.throw('Invalid API key');
expect(() => new Client({ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, apiKey: 'VF.xxxxxxxx' as any })).to.throw('Invalid API key');
expect(() => new Client({ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, apiKey: 'VF.xxxxxxxx.xxxxxxxx' as any })).not.to.throw();
});
});

describe('interact', () => {
Expand Down
12 changes: 7 additions & 5 deletions tests/lib/RuntimeClientFactory/index.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import RuntimeClientFactory, { FactoryConfig } from '@/lib/RuntimeClientFactory'
import { DEFAULT_ENDPOINT } from '@/lib/RuntimeClientFactory/constants';

import { VERSION_ID } from '../Context/fixtures';
import { API_KEY } from '../fixtures';

chai.use(chaiAsPromise);

Expand All @@ -18,7 +19,7 @@ const createRuntimeClientFactory = <S>(factoryConfig?: Partial<FactoryConfig<any
const client = sinon.stub(Client, 'default').returns(CLIENT);
const agent = sinon.stub(RuntimeClient, 'default').returns(RUNTIME_CLIENT);

const app = new RuntimeClientFactory<S>({ versionID: VERSION_ID, ...factoryConfig });
const app = new RuntimeClientFactory<S>({ versionID: VERSION_ID, apiKey: API_KEY, ...factoryConfig });

return { client, agent, app };
};
Expand All @@ -32,31 +33,32 @@ describe('RuntimeClientFactory', () => {
it('constructor', () => {
const { client } = createRuntimeClientFactory();

expect(client.args).to.eql([[{ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, variables: undefined, axiosConfig: undefined }]]);
expect(client.args).to.eql([[{ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, apiKey: API_KEY, variables: undefined, axiosConfig: undefined }]]);
});

it('variables', () => {
const { client } = createRuntimeClientFactory({ variables: 'foo' as any });

expect(client.args).to.eql([[{ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, variables: 'foo', axiosConfig: undefined }]]);
expect(client.args).to.eql([[{ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, apiKey: API_KEY, variables: 'foo', axiosConfig: undefined }]]);
});

it('axiosConfig', () => {
const { client } = createRuntimeClientFactory({ axiosConfig: 'foo' as any });

expect(client.args).to.eql([[{ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, variables: undefined, axiosConfig: 'foo' }]]);
expect(client.args).to.eql([[{ versionID: VERSION_ID, endpoint: DEFAULT_ENDPOINT, apiKey: API_KEY, variables: undefined, axiosConfig: 'foo' }]]);
});

it('optional', () => {
const { client } = createRuntimeClientFactory({
variables: 'foo' as any,
versionID: 'bar',
apiKey: 'VF.foo.bar',
endpoint: 'x',
dataConfig: 'y' as any,
axiosConfig: 'bar' as any,
});

expect(client.args).to.eql([[{ versionID: 'bar', endpoint: 'x', variables: 'foo', axiosConfig: 'bar' }]]);
expect(client.args).to.eql([[{ versionID: 'bar', endpoint: 'x', apiKey: 'VF.foo.bar', variables: 'foo', axiosConfig: 'bar' }]]);
});

it('does not accept invalid variables', () => {
Expand Down
4 changes: 3 additions & 1 deletion tests/lib/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,6 @@ export const FAKE_VISUAL_TRACE = {

export const INTERACT_ENDPOINT = (versionID: string) => `/interact/${versionID}`;

export const STATE_ENDPOINT = (versionID: string) => `/interact/${versionID}/state`;
export const STATE_ENDPOINT = (versionID: string) => `/interact/${versionID}/state`;

export const API_KEY = 'VF.xxxxxxx.xxxxxxxx';
24 changes: 24 additions & 0 deletions typings/voiceflow__verror.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
declare module '@voiceflow/verror' {
// eslint-disable-next-line import/no-extraneous-dependencies
import httpStatus, { HttpStatus } from 'http-status';

class VError {
constructor(message: string, code?: number | string, data?: any);

static HTTP_STATUS: HttpStatus;

public name: string;

public code: number | string;

public message: string;

public data: any;

public dateTime: Date;
}

export { httpStatus as HTTP_STATUS };

export default VError;
}
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,13 @@
safe-json-stringify "^1.2.0"
workerpool "^5.0.4"

"@voiceflow/verror@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@voiceflow/verror/-/verror-1.1.0.tgz#0fea1a464059f617c4051043b456dc792f837907"
integrity sha512-zxCs41ScHcqZKMuoUyjPSd+836+hyPpjrqNlXR4wEuKIhJb/dYs8f3qa0pH8N0Xq8+II1h3AEaHTGEcU3P6+Ww==
dependencies:
http-status "^1.3.2"

"@zerollup/ts-helpers@^1.7.18":
version "1.7.18"
resolved "https://registry.yarnpkg.com/@zerollup/ts-helpers/-/ts-helpers-1.7.18.tgz#747177f6d5abc06c3a0f5dffe7362d365cf0391d"
Expand Down Expand Up @@ -4237,6 +4244,11 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"

http-status@^1.3.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/http-status/-/http-status-1.5.0.tgz#2edfb02068d236ba60fd1481ad89219aa96e1677"
integrity sha512-wcGvY31MpFNHIkUcXHHnvrE4IKYlpvitJw5P/1u892gMBAM46muQ+RH7UN1d+Ntnfx5apnOnVY6vcLmrWHOLwg==

https-proxy-agent@^2.2.3:
version "2.2.4"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
Expand Down

0 comments on commit 82f3fa5

Please sign in to comment.