|
| 1 | +/* |
| 2 | + * The service worker streams every file through a port of its own to the MTProto worker, handed over |
| 3 | + * by the first tab it counts as connected. A tab it does not count - one that answered the startup check |
| 4 | + * late, one it took for gone - must still get that port, or every video hangs until the worker restarts. |
| 5 | + */ |
| 6 | +import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest'; |
| 7 | + |
| 8 | +const mocks = vi.hoisted(() => ({ |
| 9 | + invoke: vi.fn(), |
| 10 | + invokeVoid: vi.fn(), |
| 11 | + getWindowClients: vi.fn() |
| 12 | +})); |
| 13 | + |
| 14 | +vi.mock('@lib/serviceWorker/serviceMessagePort', () => ({ |
| 15 | + default: class { |
| 16 | + addMultipleEventsListeners() {} |
| 17 | + attachPort() {} |
| 18 | + detachPort() {} |
| 19 | + invoke(...args: any[]) { |
| 20 | + return mocks.invoke(...args); |
| 21 | + } |
| 22 | + |
| 23 | + invokeVoid(...args: any[]) { |
| 24 | + return mocks.invokeVoid(...args); |
| 25 | + } |
| 26 | + } |
| 27 | +})); |
| 28 | +vi.mock('@helpers/context', async(importOriginal) => ({ |
| 29 | + ...await importOriginal<typeof import('@helpers/context')>(), |
| 30 | + getWindowClients: mocks.getWindowClients |
| 31 | +})); |
| 32 | +vi.mock('@helpers/listenMessagePort', () => ({default: () => {}})); |
| 33 | +vi.mock('@lib/crypto/cryptoMessagePort', () => ({default: {attachPort: () => {}, detachPort: () => {}}})); |
| 34 | +vi.mock('@lib/serviceWorker/stream', () => ({default: () => {}, toggleStreamInUse: () => {}})); |
| 35 | +vi.mock('@lib/serviceWorker/cache', () => ({CACHE_ASSETS_NAME: 'assets', requestCache: () => {}})); |
| 36 | +vi.mock('@lib/serviceWorker/push', () => ({ |
| 37 | + closeAllNotifications: () => {}, |
| 38 | + fillPushObject: () => {}, |
| 39 | + onPing: () => {}, |
| 40 | + onShownNotification: () => {}, |
| 41 | + resetPushAccounts: () => {} |
| 42 | +})); |
| 43 | +vi.mock('@lib/files/cacheStorage', () => ({default: class {}})); |
| 44 | +vi.mock('@lib/serviceWorker/clientAccounts', () => ({deleteClientAccount: () => {}, setClientAccount: () => {}})); |
| 45 | +vi.mock('@lib/serviceWorker/download', () => ({default: () => ({onDownloadFetch: () => {}, onClosedWindows: () => {}})})); |
| 46 | +vi.mock('@lib/serviceWorker/share', () => ({default: () => {}, checkWindowClientForDeferredShare: () => {}})); |
| 47 | +vi.mock('@lib/serviceWorker/rtmp', () => ({onRtmpFetch: () => {}, onRtmpLeftCall: () => {}})); |
| 48 | +vi.mock('@lib/hls/onHlsQualityFileFetch', () => ({onHlsQualityFileFetch: () => {}})); |
| 49 | +vi.mock('@lib/hls/onHlsStreamFetch', () => ({onHlsStreamFetch: () => {}})); |
| 50 | +vi.mock('@lib/hls/onHlsPlaylistFetch', () => ({onHlsPlaylistFetch: () => {}})); |
| 51 | +vi.mock('@lib/serviceWorker/backgrounds', () => ({onBackgroundsFetch: () => {}})); |
| 52 | +vi.mock('@lib/serviceWorker/watchMtprotoOnDev', () => ({watchMtprotoOnDev: () => {}})); |
| 53 | +vi.mock('@lib/serviceWorker/clearOldCache', () => ({watchCacheStoragesLifetime: () => {}})); |
| 54 | +vi.mock('@lib/passcode/keyStore', () => ({default: {save: () => {}, resetDeferred: () => {}}})); |
| 55 | +vi.mock('@lib/passcode/deferredIsUsingPasscode', () => ({default: { |
| 56 | + resolveDeferred: () => {}, |
| 57 | + resetDeferred: () => {}, |
| 58 | + isUsingPasscodeUndeferred: () => false |
| 59 | +}})); |
| 60 | + |
| 61 | +const tab = {id: 'tab-1', type: 'window', frameType: 'top-level'}; |
| 62 | + |
| 63 | +const startWorker = () => import('@lib/serviceWorker/index.service'); |
| 64 | + |
| 65 | +const fetchFromTab = (clientId: string) => (self as any).onfetch({ |
| 66 | + request: {url: 'https://web.telegram.org/k/stream/%7B%7D', mode: 'cors', headers: new Headers()}, |
| 67 | + clientId, |
| 68 | + respondWith: () => {} |
| 69 | +}); |
| 70 | + |
| 71 | +const portsSentTo = (target: any) => mocks.invokeVoid.mock.calls.filter(([type, , source]) => type === 'port' && source === target); |
| 72 | + |
| 73 | +describe('the service worker\'s MTProto port', () => { |
| 74 | + beforeEach(() => { |
| 75 | + vi.resetModules(); |
| 76 | + vi.useFakeTimers(); |
| 77 | + mocks.invoke.mockReset().mockReturnValue(new Promise(() => {})); |
| 78 | + mocks.invokeVoid.mockReset(); |
| 79 | + mocks.getWindowClients.mockReset().mockResolvedValue([]); |
| 80 | + (self as any).clients = {get: vi.fn(async(id: string) => id === tab.id ? tab : undefined)}; |
| 81 | + }); |
| 82 | + |
| 83 | + afterEach(() => { |
| 84 | + vi.useRealTimers(); |
| 85 | + delete (self as any).clients; |
| 86 | + delete (self as any).onfetch; |
| 87 | + }); |
| 88 | + |
| 89 | + test('goes to a tab that answers the startup check late', async() => { |
| 90 | + let answer: () => void; |
| 91 | + mocks.invoke.mockImplementation((type: string) => type === 'hello' ? |
| 92 | + new Promise<void>((resolve) => answer = resolve) : |
| 93 | + new Promise(() => {})); |
| 94 | + mocks.getWindowClients.mockResolvedValue([tab]); |
| 95 | + |
| 96 | + await startWorker(); |
| 97 | + await vi.advanceTimersByTimeAsync(0); |
| 98 | + expect(mocks.invoke).toHaveBeenCalledWith('hello', undefined, undefined, tab); |
| 99 | + |
| 100 | + // * the tab is busy or frozen while the worker starts, and answers well after it |
| 101 | + await vi.advanceTimersByTimeAsync(30e3); |
| 102 | + expect(portsSentTo(tab)).toHaveLength(0); |
| 103 | + answer(); |
| 104 | + await vi.advanceTimersByTimeAsync(0); |
| 105 | + |
| 106 | + expect(portsSentTo(tab), 'the late answer was dropped').toHaveLength(1); |
| 107 | + }); |
| 108 | + |
| 109 | + test('goes to a tab the worker does not count with its next request', async() => { |
| 110 | + await startWorker(); |
| 111 | + await vi.advanceTimersByTimeAsync(0); |
| 112 | + |
| 113 | + fetchFromTab(tab.id); |
| 114 | + await vi.advanceTimersByTimeAsync(0); |
| 115 | + |
| 116 | + expect(portsSentTo(tab), 'the request hangs with no port').toHaveLength(1); |
| 117 | + expect(mocks.invokeVoid).toHaveBeenCalledWith('hello', undefined, tab); |
| 118 | + }); |
| 119 | + |
| 120 | + test('costs a connected tab\'s requests nothing and is not sent twice', async() => { |
| 121 | + await startWorker(); |
| 122 | + await vi.advanceTimersByTimeAsync(0); |
| 123 | + fetchFromTab(tab.id); |
| 124 | + await vi.advanceTimersByTimeAsync(0); |
| 125 | + const getClient = (self as any).clients.get; |
| 126 | + getClient.mockClear(); |
| 127 | + |
| 128 | + fetchFromTab(tab.id); |
| 129 | + fetchFromTab(tab.id); |
| 130 | + await vi.advanceTimersByTimeAsync(0); |
| 131 | + |
| 132 | + expect(getClient).not.toHaveBeenCalled(); |
| 133 | + expect(portsSentTo(tab)).toHaveLength(1); |
| 134 | + }); |
| 135 | + |
| 136 | + test('is not handed to anything but a window', async() => { |
| 137 | + const worker = {id: 'worker-1', type: 'sharedworker'}; |
| 138 | + (self as any).clients.get = vi.fn(async() => worker); |
| 139 | + await startWorker(); |
| 140 | + await vi.advanceTimersByTimeAsync(0); |
| 141 | + |
| 142 | + fetchFromTab(worker.id); |
| 143 | + fetchFromTab(''); |
| 144 | + await vi.advanceTimersByTimeAsync(0); |
| 145 | + |
| 146 | + expect(mocks.invokeVoid.mock.calls.filter(([type]) => type === 'port')).toHaveLength(0); |
| 147 | + }); |
| 148 | +}); |
0 commit comments