Skip to content
This repository has been archived by the owner on Jul 20, 2022. It is now read-only.

Commit

Permalink
feat: allow private key to be read from file
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewLeedham committed Nov 26, 2019
1 parent f3c002a commit 25ac277
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,19 @@ Array [
},
]
`;

exports[`private key from file 1`] = `
Array [
"https://analytics.adobe.io/api/---global-id---/reports",
Object {
"body": "{\\"rsid\\":\\"---rsid---\\",\\"globalFilters\\":[{\\"type\\":\\"dateRange\\",\\"dateRange\\":\\"2019-08-01T00:00:00.000/2019-11-01T00:00:00.000\\"}],\\"metricContainer\\":{\\"metrics\\":[{\\"columnId\\":\\"0\\",\\"id\\":\\"metrics/visitors\\"}]},\\"dimension\\":\\"variables/browser\\",\\"settings\\":{\\"countRepeatInstances\\":true,\\"limit\\":50,\\"page\\":0,\\"nonesBehavior\\":\\"return-nones\\"}}",
"headers": Object {
"Authorization": "Bearer ---access-token---",
"Content-Type": "application/json",
"x-api-key": "---client-id---",
"x-proxy-global-company-id": "---global-id---",
},
"method": "post",
},
]
`;
72 changes: 68 additions & 4 deletions source/library/__tests__/getAnalyticsResponse.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
jest.mock('util');

import { mocked } from 'ts-jest/utils';
import nodeFetch, { Response } from 'node-fetch';
import authenticate from '@adobe/jwt-auth';
import mockBrowserReport from '../../__specs__/browser-report';
import mockOptions from '../../__specs__/options';
import * as util from 'util';
import MockDate from 'mockdate';
// eslint-disable-next-line jest/no-mocks-import
import { FetchError } from '../../__mocks__/node-fetch';

import getAnalyticsResponse from '../getAnalyticsResponse';
import { ResponseError } from '../../types';

const mockFetch = mocked(nodeFetch, true);
const mockAuthenticate = mocked(authenticate, true);
MockDate.set('2019-11-01T00:00:00.000');
const mockConsoleError = jest.fn();
const mockReadFile = jest.fn();
mocked(util).promisify.mockImplementation(() => mockReadFile);

import getAnalyticsResponse from '../getAnalyticsResponse';

let originalConsoleError: typeof console.error;
beforeAll(() => {
Expand All @@ -40,10 +45,11 @@ test('normal behaviour', async () => {
...mockOptions,
metaScopes: ['ent_analytics_bulk_ingest_sdk'],
});
expect(mockReadFile).not.toHaveBeenCalled();
});

test('HTTP error', async () => {
mockFetch.mockImplementation(
mockFetch.mockImplementationOnce(
async (): Promise<Response> =>
(({
json: async (): Promise<Response> => {
Expand All @@ -64,11 +70,12 @@ test('HTTP error', async () => {
expect(mockConsoleError).toHaveBeenCalledWith(
new ResponseError('Forbidden', 403)
);
expect(mockReadFile).not.toHaveBeenCalled();
});

test('Fetch error', async () => {
const mockFetchError = new FetchError('---message---', '---type---');
mockFetch.mockImplementation(
mockFetch.mockImplementationOnce(
async (): Promise<Response> =>
(({
json: async (): Promise<Response> => {
Expand All @@ -85,4 +92,61 @@ test('Fetch error', async () => {
metaScopes: ['ent_analytics_bulk_ingest_sdk'],
});
expect(mockConsoleError).toHaveBeenCalledWith(mockFetchError);
expect(mockReadFile).not.toHaveBeenCalled();
});

test('private key from file', async () => {
mockReadFile.mockImplementationOnce(() => {
return '---private-key-from-file---';
});
await expect(
getAnalyticsResponse({
...mockOptions,
privateKey: undefined,
privateKeyPath: '/path/to/private.key',
})
).resolves.toEqual(mockBrowserReport);
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch.mock.calls[0]).toMatchSnapshot();
expect(mockAuthenticate).toHaveBeenCalledTimes(1);
expect(mockAuthenticate).toHaveBeenCalledWith({
...mockOptions,
privateKey: '---private-key-from-file---',
privateKeyPath: '/path/to/private.key',
metaScopes: ['ent_analytics_bulk_ingest_sdk'],
});
expect(mockReadFile).toHaveBeenCalledTimes(1);
});

test('no private key', async () => {
await expect(
getAnalyticsResponse({
...mockOptions,
privateKey: undefined,
privateKeyPath: '/path/to/private.key',
})
).rejects.toThrow(
new Error(
'Invalid private key either pass the raw key via `privateKey` or a path to it via `privateKeyPath`.'
)
);
expect(mockFetch).not.toHaveBeenCalled();
expect(mockAuthenticate).not.toHaveBeenCalled();
expect(mockReadFile).toHaveBeenCalledTimes(1);
});

test('no private or private key path', async () => {
await expect(
getAnalyticsResponse({
...mockOptions,
privateKey: undefined,
} as any)
).rejects.toThrow(
new Error(
'Invalid private key either pass the raw key via `privateKey` or a path to it via `privateKeyPath`.'
)
);
expect(mockFetch).not.toHaveBeenCalled();
expect(mockAuthenticate).not.toHaveBeenCalled();
expect(mockReadFile).not.toHaveBeenCalled();
});
21 changes: 20 additions & 1 deletion source/library/getAnalyticsResponse.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import authorize, { JWTAuthConfig } from '@adobe/jwt-auth';
import { BaseOptions, ResponseError, RankedReportData } from '../types';
import {
BaseOptions,
ResponseError,
RankedReportData,
hasPrivateKey,
} from '../types';
import fetch from 'node-fetch';
import getRequestBody from './getRequestBody';
import fs from 'fs';
import { promisify } from 'util';

const readFile = promisify(fs.readFile);

/**
* Pulls browser data from Adobe Analytics.
Expand All @@ -12,8 +21,18 @@ import getRequestBody from './getRequestBody';
export default async function getAnalyticsResponse(
options: BaseOptions
): Promise<RankedReportData | undefined> {
const privateKey =
(hasPrivateKey(options) && options.privateKey) ||
(options.privateKeyPath &&
(await readFile(options.privateKeyPath))?.toString());
if (!privateKey) {
throw new Error(
'Invalid private key either pass the raw key via `privateKey` or a path to it via `privateKeyPath`.'
);
}
const config: JWTAuthConfig = {
...options,
privateKey,
metaScopes: ['ent_analytics_bulk_ingest_sdk'],
};
try {
Expand Down
35 changes: 32 additions & 3 deletions source/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { JWTAuthConfig } from '@adobe/jwt-auth';
import { duration } from 'moment';

export interface BaseOptions extends Omit<JWTAuthConfig, 'metaScopes'> {
interface BaseBaseOptions
extends Omit<JWTAuthConfig, 'metaScopes' | 'privateKey'> {
privateKeyPath?: string;
rsid: string;
globalId: string;
duration?: Parameters<typeof duration>;
Expand All @@ -10,10 +12,37 @@ export interface BaseOptions extends Omit<JWTAuthConfig, 'metaScopes'> {
limit?: number;
}

export interface WriteOptions extends BaseOptions {
export type BaseOptionsWithPrivateKey = BaseBaseOptions & {
privateKey: string;
};
export type BaseOptionsWithPrivateKeyPath = BaseBaseOptions & {
privateKeyPath: string;
};

export type BaseOptions =
| BaseOptionsWithPrivateKey
| BaseOptionsWithPrivateKeyPath;

/**
* Type guard to check if we have a private key or private key path.
*
* @param options - Options object to check.
* @returns Whether `privateKey` or `privateKeyPath` is present.
*/
export function hasPrivateKey(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options: any
): options is BaseOptionsWithPrivateKey {
if (options.privateKey) {
return true;
}
return false;
}

export type WriteOptions = BaseOptions & {
cwd?: string;
filename?: string;
}
};

export interface RankedReportData {
totalPages: number;
Expand Down

0 comments on commit 25ac277

Please sign in to comment.