diff --git a/code/core/src/common/utils/get-storybook-refs.test.ts b/code/core/src/common/utils/get-storybook-refs.test.ts new file mode 100644 index 000000000000..9c1c4f540945 --- /dev/null +++ b/code/core/src/common/utils/get-storybook-refs.test.ts @@ -0,0 +1,35 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { checkRef } from './get-storybook-refs'; + +describe('checkRef', () => { + afterEach(() => vi.restoreAllMocks()); + + it('returns true when fetch returns 200', async () => { + vi.spyOn(global, 'fetch').mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({}), + } as Response); + expect(await checkRef('https://chromatic.com')).toBe(true); + }); + + it('returns false when fetch returns 401', async () => { + vi.spyOn(global, 'fetch').mockResolvedValue({ ok: false, status: 401 } as Response); + expect(await checkRef('https://chromatic.com')).toBe(false); + }); + + it('returns false when fetch returns 200 with loginUrl', async () => { + vi.spyOn(global, 'fetch').mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ loginUrl: 'https://chromatic.com/login' }), + } as Response); + expect(await checkRef('https://chromatic.com')).toBe(false); + }); + + it('returns false when fetch fails', async () => { + vi.spyOn(global, 'fetch').mockRejectedValue(new Error('Network error')); + expect(await checkRef('https://chromatic.com')).toBe(false); + }); +}); diff --git a/code/core/src/common/utils/get-storybook-refs.ts b/code/core/src/common/utils/get-storybook-refs.ts index 457025541997..c6e1d10d1427 100644 --- a/code/core/src/common/utils/get-storybook-refs.ts +++ b/code/core/src/common/utils/get-storybook-refs.ts @@ -58,7 +58,7 @@ export const getAutoRefs = async (options: Options): Promise ); }; -const checkRef = (url: string) => +export const checkRef = (url: string) => fetch(`${url}/iframe.html`).then( async ({ ok, status }) => { if (ok) { diff --git a/code/core/src/manager-api/modules/refs.ts b/code/core/src/manager-api/modules/refs.ts index 586453443c05..151003529eed 100644 --- a/code/core/src/manager-api/modules/refs.ts +++ b/code/core/src/manager-api/modules/refs.ts @@ -118,6 +118,17 @@ async function handleRequest( throw new Error('Unexpected boolean response'); } if (!response.ok) { + // Check for 401 responses that may contain loginUrl + if (response.status === 401) { + try { + const json = await response.json(); + if (json.loginUrl) { + return { loginUrl: json.loginUrl }; + } + } catch { + // Fall through to error handling if JSON parsing fails + } + } throw new Error(`Unexpected response not OK: ${response.statusText}`); } diff --git a/code/core/src/manager-api/tests/refs.test.ts b/code/core/src/manager-api/tests/refs.test.ts index bc337d3a7497..d3ec49e7cf7b 100644 --- a/code/core/src/manager-api/tests/refs.test.ts +++ b/code/core/src/manager-api/tests/refs.test.ts @@ -73,6 +73,7 @@ function createMockStore(initialState: Partial = {}) { interface ResponseResult { ok?: boolean; + status?: number; err?: Error; response?: () => never | object | Promise; } @@ -86,13 +87,14 @@ type ResponseKeys = | 'metadata'; function respond(result: ResponseResult): Promise { - const { err, ok, response } = result; + const { err, ok, status, response } = result; if (err) { return Promise.reject(err); } return Promise.resolve({ ok: ok ?? !!response, + status: status ?? (ok ? 200 : 500), json: response, } as Response); } @@ -784,6 +786,52 @@ describe('Refs API', () => { `); }); + it('checks refs (auth with 401)', async () => { + // given + const { api } = initRefs({ provider, store } as any, { runCheck: false }); + + setupResponses({ + indexPrivate: { + ok: false, + status: 401, + response: async () => ({ loginUrl: 'https://example.com/login' }), + }, + storiesPrivate: { + ok: false, + status: 401, + response: async () => ({ loginUrl: 'https://example.com/login' }), + }, + metadata: { + ok: false, + status: 401, + response: async () => ({ loginUrl: 'https://example.com/login' }), + }, + }); + + await api.checkRef({ + id: 'fake', + url: 'https://example.com', + title: 'Fake', + }); + + expect(store.setState.mock.calls[0][0]).toMatchInlineSnapshot(` + { + "refs": { + "fake": { + "filteredIndex": undefined, + "id": "fake", + "index": undefined, + "internal_index": undefined, + "loginUrl": "https://example.com/login", + "title": "Fake", + "type": "auto-inject", + "url": "https://example.com", + }, + }, + } + `); + }); + it('checks refs (basic-auth)', async () => { // given const { api } = initRefs({ provider, store } as any, { runCheck: false });