diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index dde52cda49..788d7e3fed 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -17,16 +17,15 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; -import { EventSourcePolyfill } from 'event-source-polyfill'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; import { MockConfigApi, MockFetchApi } from '@backstage/test-utils'; import { TechDocsStorageClient } from './client'; -const MockedEventSource = EventSourcePolyfill as jest.MockedClass< - typeof EventSourcePolyfill +jest.mock('@microsoft/fetch-event-source'); +const mockFetchEventSource = fetchEventSource as jest.MockedFunction< + typeof fetchEventSource >; -jest.mock('event-source-polyfill'); - const mockEntity = { kind: 'Component', namespace: 'default', @@ -51,7 +50,6 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); @@ -72,7 +70,6 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); @@ -84,71 +81,49 @@ describe('TechDocsStorageClient', () => { }); describe('syncEntityDocs', () => { - it('should create eventsource without headers', async () => { + it('should create eventsource with fetch', async () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } - }, - ); - + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); + }); identityApi.getCredentials.mockResolvedValue({}); await storageApi.syncEntityDocs(mockEntity); - expect(MockedEventSource).toHaveBeenCalledWith( + expect(mockFetchEventSource).toHaveBeenCalledWith( 'http://backstage:9191/api/techdocs/sync/default/Component/test-component', - { withCredentials: true, headers: {} }, - ); - }); - - it('should create eventsource with headers', async () => { - const storageApi = new TechDocsStorageClient({ - configApi, - discoveryApi, - identityApi, - fetchApi, - }); - - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } + { + fetch: fetchApi.fetch, + onerror: expect.any(Function), + onmessage: expect.any(Function), }, ); - - identityApi.getCredentials.mockResolvedValue({ token: 'token' }); - await storageApi.syncEntityDocs(mockEntity); - - expect(MockedEventSource).toHaveBeenCalledWith( - 'http://backstage:9191/api/techdocs/sync/default/Component/test-component', - { withCredentials: true, headers: { Authorization: 'Bearer token' } }, - ); }); it('should resolve to cached', async () => { + const myFetchApi = new MockFetchApi({ + injectIdentityAuth: { identityApi }, + }); const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, - fetchApi, + fetchApi: myFetchApi, + }); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } - }, - ); identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( @@ -160,17 +135,15 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); - - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": true}' } as any); - } - }, - ); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": true}' }); + }); identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( @@ -182,21 +155,18 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); - MockedEventSource.prototype.addEventListener.mockImplementation( - (type, fn) => { - if (type === 'log' && typeof fn === 'function') { - fn({ data: '"A log message"' } as any); - } - - if (type === 'finish' && typeof fn === 'function') { - fn({ data: '{"updated": false}' } as any); - } - }, - ); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onopen, onmessage } = options; + await Promise.resolve(); + await onopen?.({ ok: true } as Response); + await Promise.resolve(); + onmessage?.({ id: '', event: 'log', data: '"A log message"' }); + await Promise.resolve(); + onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); + }); identityApi.getCredentials.mockResolvedValue({}); const logHandler = jest.fn(); @@ -212,25 +182,18 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onerror } = options; + onerror?.(new NotFoundError('Some not found warning')); + }); + // we await later after we emitted the error identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); - // flush the event loop - await new Promise(r => setTimeout(r)); - - const instance = MockedEventSource.mock - .instances[0] as jest.Mocked; - - instance.onerror?.({ - status: 404, - message: 'Some not found warning', - } as any); - await expect(promise).rejects.toThrow(NotFoundError); await expect(promise).rejects.toThrow('Some not found warning'); }); @@ -239,7 +202,6 @@ describe('TechDocsStorageClient', () => { const storageApi = new TechDocsStorageClient({ configApi, discoveryApi, - identityApi, fetchApi, }); @@ -247,16 +209,10 @@ describe('TechDocsStorageClient', () => { identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); - // flush the event loop - await new Promise(r => setTimeout(r)); - - const instance = MockedEventSource.mock - .instances[0] as jest.Mocked; - - instance.onerror?.({ - type: 'error', - data: 'Some other error', - } as any); + mockFetchEventSource.mockImplementation(async (_url, options) => { + const { onerror } = options; + onerror?.(new Error('Some other error')); + }); await expect(promise).rejects.toThrow(Error); await expect(promise).rejects.toThrow('Some other error'); diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 76338a8f1e..b79c140750 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -209,24 +209,22 @@ export class TechDocsStorageClient implements TechDocsStorageApi { return new Promise((resolve, reject) => { fetchEventSource(url, { - withCredentials: true, fetch: this.fetchApi.fetch, onmessage(e: any) { - if (e.data) { + if (e.event === 'log') { logHandler(JSON.parse(e.data)); + } else if (e.event === 'finish') { + let updated: boolean = false; + if (e.data) { + ({ updated } = JSON.parse(e.data)); + } + resolve(updated ? 'updated' : 'cached'); + } else if (e.event === 'error') { + reject(new Error(e.data)); } }, - onclose(e: any) { - let updated: boolean = false; - - if (e.data) { - ({ updated } = JSON.parse(e.data)); - } - - resolve(updated ? 'updated' : 'cached'); - }, - onerror(e: any) { - reject(e); + onerror(err) { + reject(err); }, }); });