Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composition: Verify refs in node #12085

Merged
merged 10 commits into from
Aug 20, 2020
34 changes: 14 additions & 20 deletions lib/api/src/modules/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface ComposedRef {
id: string;
title?: string;
url: string;
type?: 'auto-inject' | 'unknown' | 'lazy';
type?: 'auto-inject' | 'unknown' | 'lazy' | 'server-checked';
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
stories: StoriesHash;
versions?: Versions;
loginUrl?: string;
Expand All @@ -46,7 +46,7 @@ export interface ComposedRef {
}
export interface ComposedRefUpdate {
title?: string;
type?: 'auto-inject' | 'unknown' | 'lazy';
type?: 'auto-inject' | 'unknown' | 'lazy' | 'server-checked';
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
stories?: StoriesHash;
versions?: Versions;
loginUrl?: string;
Expand Down Expand Up @@ -135,28 +135,27 @@ export const init: ModuleFn = ({ store, provider, fullAPI }, { runCheck = true }
});
},
checkRef: async (ref) => {
const { id, url, version } = ref;
const { id, url, version, type } = ref;
const isPublic = type === 'server-checked';

const loadedData: { error?: Error; stories?: StoriesRaw; loginUrl?: string } = {};
const query = version ? `?version=${version}` : '';

const [included, omitted, iframe] = await allSettled([
fetch(`${url}/stories.json${query}`, {
headers: {
Accept: 'application/json',
},
credentials: 'include',
}),
const [included, omitted] = await allSettled([
isPublic
? Promise.resolve(false)
: fetch(`${url}/stories.json${query}`, {
headers: {
Accept: 'application/json',
},
credentials: 'include',
}),
fetch(`${url}/stories.json${query}`, {
headers: {
Accept: 'application/json',
},
credentials: 'omit',
}),
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
fetch(`${url}/iframe.html${query}`, {
mode: 'no-cors',
credentials: 'omit',
}),
]);

const handle = async (request: Response | false): Promise<SetRefData> => {
Expand All @@ -168,7 +167,7 @@ export const init: ModuleFn = ({ store, provider, fullAPI }, { runCheck = true }
return {};
};

if (!included && !omitted && !iframe) {
if (!included && !omitted && !isPublic) {
loadedData.error = {
message: dedent`
Error: Loading of ref failed
Expand Down Expand Up @@ -244,11 +243,6 @@ export const init: ModuleFn = ({ store, provider, fullAPI }, { runCheck = true }

const initialState: SubState['refs'] = refs;

Object.values(refs).forEach((r) => {
// eslint-disable-next-line no-param-reassign
r.type = 'unknown';
});

if (runCheck) {
Object.entries(refs).forEach(([k, v]) => {
api.checkRef(v as SetRefData);
Expand Down
127 changes: 76 additions & 51 deletions lib/api/src/tests/refs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,6 @@ describe('Refs API', () => {
},
},
],
Array [
"https://example.com/iframe.html",
Object {
"credentials": "omit",
"mode": "no-cors",
},
],
]
`);
});
Expand Down Expand Up @@ -200,13 +193,6 @@ describe('Refs API', () => {
},
},
],
Array [
"https://example.com/iframe.html?version=2.1.3-rc.2",
Object {
"credentials": "omit",
"mode": "no-cors",
},
],
]
`);
});
Expand Down Expand Up @@ -262,13 +248,6 @@ describe('Refs API', () => {
},
},
],
Array [
"https://example.com/iframe.html",
Object {
"credentials": "omit",
"mode": "no-cors",
},
],
]
`);

Expand Down Expand Up @@ -352,13 +331,6 @@ describe('Refs API', () => {
},
},
],
Array [
"https://example.com/iframe.html",
Object {
"credentials": "omit",
"mode": "no-cors",
},
],
Array [
"https://example.com/metadata.json",
Object {
Expand Down Expand Up @@ -445,13 +417,6 @@ describe('Refs API', () => {
},
},
],
Array [
"https://example.com/iframe.html",
Object {
"credentials": "omit",
"mode": "no-cors",
},
],
Array [
"https://example.com/metadata.json",
Object {
Expand Down Expand Up @@ -539,13 +504,6 @@ describe('Refs API', () => {
},
},
],
Array [
"https://example.com/iframe.html",
Object {
"credentials": "omit",
"mode": "no-cors",
},
],
Array [
"https://example.com/metadata.json",
Object {
Expand Down Expand Up @@ -580,7 +538,70 @@ describe('Refs API', () => {
`);
});

it('checks refs (cors)', async () => {
it('checks refs (serverside-success)', async () => {
// given
const { api } = initRefs({ provider, store }, { runCheck: false });

setupResponses(
{
ok: false,
response: async () => {
throw new Error('Failed to fetch');
},
},
{
ok: true,
response: async () => {
throw new Error('not ok');
},
},
{
ok: false,
response: async () => {
throw new Error('Failed to fetch');
},
}
);

await api.checkRef({
id: 'fake',
url: 'https://example.com',
title: 'Fake',
type: 'server-checked',
});

expect(fetch.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"https://example.com/stories.json",
Object {
"credentials": "omit",
"headers": Object {
"Accept": "application/json",
},
},
],
]
`);

expect(store.setState.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"refs": Object {
"fake": Object {
"error": undefined,
"id": "fake",
"ready": false,
"stories": undefined,
"title": "Fake",
"type": "auto-inject",
"url": "https://example.com",
},
},
}
`);
});

it('checks refs (serverside-fail)', async () => {
// given
const { api } = initRefs({ provider, store }, { runCheck: false });

Expand Down Expand Up @@ -615,6 +636,7 @@ describe('Refs API', () => {
id: 'fake',
url: 'https://example.com',
title: 'Fake',
type: 'unknown',
});

expect(fetch.mock.calls).toMatchInlineSnapshot(`
Expand All @@ -637,21 +659,24 @@ describe('Refs API', () => {
},
},
],
Array [
"https://example.com/iframe.html",
Object {
"credentials": "omit",
"mode": "no-cors",
},
],
]
`);

expect(store.setState.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"refs": Object {
"fake": Object {
"error": undefined,
"error": Object {
"message": "Error: Loading of ref failed
at fetch (lib/api/src/modules/refs.ts)

URL: https://example.com

We weren't able to load the above URL,
it's possible a CORS error happened.

Please check your dev-tools network tab.",
},
"id": "fake",
"ready": false,
"stories": undefined,
Expand Down
16 changes: 16 additions & 0 deletions lib/core/src/server/manager/manager-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'path';
import fs from 'fs-extra';
import findUp from 'find-up';
import resolveFrom from 'resolve-from';
import fetch from 'node-fetch';

import { logger } from '@storybook/node-logger';

Expand Down Expand Up @@ -36,6 +37,12 @@ const getAutoRefs = async (options) => {
return list.filter(Boolean);
};

const checkRef = (url) =>
fetch(`${url}/iframe.html`).then(
({ ok }) => ok,
() => false
);

const stripTrailingSlash = (url) => url.replace(/\/$/, '');

const toTitle = (input) => {
Expand Down Expand Up @@ -91,6 +98,15 @@ async function getManagerWebpackConfig(options, presets) {
}
if (autoRefs || definedRefs) {
entries.push(path.resolve(path.join(options.configDir, `generated-refs.js`)));

// verify the refs are publicly reachable, if they are not we'll require stories.json at runtime, otherwise the ref won't work
await Promise.all(
Object.entries(refs).map(async ([k, value]) => {
const ok = checkRef(value.url);

refs[k] = { ...value, type: ok ? 'server-checked' : 'unknown' };
})
);
}

return presets.apply('managerWebpack', {}, { ...options, babelOptions, entries, refs });
Expand Down