Skip to content

Commit

Permalink
Merge pull request #14 from enduire/apikey-auth
Browse files Browse the repository at this point in the history
Use temporary pull-request token for PRs from forks
  • Loading branch information
trotzig authored Jun 16, 2018
2 parents bf99c50 + cb384b2 commit e0a6a9e
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 20 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "happo.io",
"version": "1.4.0",
"version": "1.5.0",
"description": "Visual diffing for UI components",
"main": "./build/index.js",
"bin": {
Expand Down
8 changes: 4 additions & 4 deletions src/executeCli.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ commander
if (commander.only) {
usedSha = `${usedSha}-${commander.only}`;
}
await runCommand(usedSha, loadUserConfig(commander.config), {
await runCommand(usedSha, await loadUserConfig(commander.config), {
only: commander.only,
link: commander.link,
message: commander.message,
Expand All @@ -41,7 +41,7 @@ commander
.command('dev')
.description('start dev mode')
.action(async () => {
await devCommand(loadUserConfig(commander.config), {
await devCommand(await loadUserConfig(commander.config), {
only: commander.only,
});
});
Expand All @@ -50,7 +50,7 @@ commander
.command('has-report <sha>')
.description('check if there is a report for a specific sha')
.action(async (sha) => {
if (await hasReportCommand(sha, loadUserConfig(commander.config))) {
if (await hasReportCommand(sha, await loadUserConfig(commander.config))) {
process.exit(0);
} else {
process.exit(1);
Expand All @@ -61,7 +61,7 @@ commander
.command('compare <sha1> <sha2>')
.description('compare reports for two different shas')
.action(async (sha1, sha2) => {
const result = await compareReportsCommand(sha1, sha2, loadUserConfig(commander.config), {
const result = await compareReportsCommand(sha1, sha2, await loadUserConfig(commander.config), {
link: commander.link,
message: commander.message,
author: commander.author,
Expand Down
36 changes: 31 additions & 5 deletions src/loadUserConfig.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import request from 'request-promise-native';
import requireRelative from 'require-relative';

import WrappedError from './WrappedError';
import * as defaultConfig from './DEFAULTS';

function load(pathToConfigFile) {
Expand All @@ -15,13 +17,37 @@ function load(pathToConfigFile) {
}
}

export default function loadUserConfig(pathToConfigFile) {
async function getPullRequestSecret({ endpoint }, env) {
const { secret } = await request({
url: `${endpoint}/api/pull-request-token`,
method: 'POST',
json: true,
body: {
prUrl: env.CHANGE_URL,
},
});

return secret;
}

export default async function loadUserConfig(pathToConfigFile, env = process.env) {
const { CHANGE_URL } = env;

const config = load(pathToConfigFile);
if (!config.apiKey || !config.apiSecret) {
throw new Error(
'You need an `apiKey` and `apiSecret` in your config. ' +
'To obtain one, go to https://happo.io/me',
);
if (!CHANGE_URL) {
throw new Error(
'You need an `apiKey` and `apiSecret` in your config. ' +
'To obtain one, go to https://happo.io/settings',
);
}
try {
// Reassign api tokens to temporary once provided for the PR
config.apiKey = CHANGE_URL;
config.apiSecret = await getPullRequestSecret(config, env);
} catch (e) {
throw new WrappedError('Failed to obtain temporary pull-request token', e);
}
}
if (!config.targets || Object.keys(config.targets).length === 0) {
throw new Error(
Expand Down
8 changes: 7 additions & 1 deletion src/processSnapsInBundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,13 @@ export default async function processSnapsInBundle(
const onlyComponent = only ? only.split('#')[1] : undefined;

await queued(Object.keys(dom.window.snaps), async (fileName) => {
const objectOrArray = dom.window.snaps[fileName];
let objectOrArray = dom.window.snaps[fileName];
const keys = Object.keys(objectOrArray);
if (keys.includes('default') && Array.isArray(objectOrArray.default)) {
// The default export is an array. Treat this as a file which has
// generated examples.
objectOrArray = objectOrArray.default;
}
if (Array.isArray(objectOrArray)) {
await queued(objectOrArray, async ({ component, variants }) => {
const processedVariants = await processVariants({
Expand Down
11 changes: 11 additions & 0 deletions test/integrations/examples/Bar-react-happo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';

export default [
{
component: 'Bar',
variants: {
one: () => <div>one</div>,
two: () => <div>two</div>,
},
},
];
14 changes: 14 additions & 0 deletions test/integrations/react-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ beforeEach(() => {
it('produces the right html', async () => {
await subject();
expect(config.targets.chrome.snapPayloads).toEqual([
{
component: 'Bar',
css: '',
hash: 'a2e67882fb930e9fa82a3bb3e442ca23',
html: '<div>one</div>',
variant: 'one',
},
{
component: 'Bar',
css: '',
hash: 'e365ab0df4cb21bba69c2c34d8162127',
html: '<div>two</div>',
variant: 'two',
},
{
component: 'Foo-react',
css: '',
Expand Down
49 changes: 40 additions & 9 deletions test/loadUserConfig-test.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,67 @@
import requireRelative from 'require-relative';
import request from 'request-promise-native';

import RemoteBrowserTarget from '../src/RemoteBrowserTarget';
import loadUserConfig from '../src/loadUserConfig';

jest.mock('request-promise-native');
jest.mock('require-relative');

it('yells if api tokens are missing', () => {
it('yells if api tokens are missing', async () => {
requireRelative.mockImplementation(() => ({}));
expect(() => loadUserConfig('bogus')).toThrowError(/You need an `apiKey`/);
await expect(loadUserConfig('bogus')).rejects.toThrow(/You need an `apiKey`/);
});

it('yells if targets are missing', () => {
it('yells if targets are missing', async () => {
requireRelative.mockImplementation(() => ({
apiKey: '1',
apiSecret: '2',
targets: {},
}));
expect(() => loadUserConfig('bogus')).toThrowError(/You need at least one target/);
await expect(loadUserConfig('bogus')).rejects.toThrow(/You need at least one target/);
});

it('does not yell if all required things are in place', () => {
it('does not yell if all required things are in place', async () => {
requireRelative.mockImplementation(() => ({
apiKey: '1',
apiSecret: '2',
targets: {
firefox: new RemoteBrowserTarget('firefox', { viewport: '800x600' }),
},
}));
expect(() => loadUserConfig('bogus')).not.toThrowError();
expect(loadUserConfig('bogus').apiKey).toEqual('1');
expect(loadUserConfig('bogus').apiSecret).toEqual('2');
expect(loadUserConfig('bogus').targets).toEqual({
const config = await loadUserConfig('bogus');
expect(config.apiKey).toEqual('1');
expect(config.apiSecret).toEqual('2');
expect(config.targets).toEqual({
firefox: new RemoteBrowserTarget('firefox', { viewport: '800x600' }),
});
});

describe('when CHANGE_URL is defined', () => {
beforeEach(() => {
requireRelative.mockImplementation(() => ({
targets: {
firefox: new RemoteBrowserTarget('firefox', { viewport: '800x600' }),
},
}));
request.mockImplementation(() => Promise.resolve({ secret: 'yay' }));
});

it('grabs a temporary secret', async () => {
const config = await loadUserConfig('bogus', { CHANGE_URL: 'foo.bar' });
expect(config.apiKey).toEqual('foo.bar');
expect(config.apiSecret).toEqual('yay');
});

describe('when the API has an error response', () => {
beforeEach(() => {
request.mockImplementation(() => Promise.reject(new Error('nope')));
});

it('yells', async () => {
await expect(loadUserConfig('bogus', { CHANGE_URL: 'foo.bar' })).rejects.toThrow(
/Failed to obtain temporary pull-request token/,
);
});
});
});

0 comments on commit e0a6a9e

Please sign in to comment.