1/* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {WindowUtils} from 'common/window_utils'; 18import {UnitTestUtils} from 'test/unit/utils'; 19import {ConnectionState} from 'trace_collection/connection_state'; 20import {ConnectionStateListener} from 'trace_collection/connection_state_listener'; 21import {DevicesStream} from './devices_stream'; 22import {StreamProvider} from './stream_provider'; 23import {WdpDeviceConnection} from './wdp_device_connection'; 24import { 25 WdpDeviceConnectionResponse, 26 WdpHostConnection, 27 WdpRequestDevicesResponse, 28} from './wdp_host_connection'; 29 30describe('WdpHostConnection', () => { 31 const listener = jasmine.createSpyObj<ConnectionStateListener>( 32 'ConnectionStateListener', 33 [ 34 'onAvailableTracesChange', 35 'onError', 36 'onConnectionStateChange', 37 'onDevicesChange', 38 ], 39 ); 40 const testApproveUrl = 'test_approve_url'; 41 let connection: WdpHostConnection; 42 let devicesStreamSpy: jasmine.Spy; 43 let popupSpy: jasmine.Spy; 44 45 beforeAll(() => { 46 devicesStreamSpy = spyOn(StreamProvider.prototype, 'createDevicesStream'); 47 }); 48 49 beforeEach(() => { 50 popupSpy = spyOn(WindowUtils, 'showPopupWindow'); 51 connection = new WdpHostConnection(listener); 52 resetListener(); 53 }); 54 55 afterEach(() => { 56 expect(listener.onAvailableTracesChange).not.toHaveBeenCalled(); 57 expect(listener.onError).not.toHaveBeenCalled(); 58 expect(listener.onConnectionStateChange).not.toHaveBeenCalled(); 59 expect(listener.onDevicesChange).not.toHaveBeenCalled(); 60 }); 61 62 describe('initialization and destruction:', () => { 63 it('closes all streams onDestroy', async () => { 64 const spy = spyOn(StreamProvider.prototype, 'closeAllStreams'); 65 connection.onDestroy(); 66 expect(spy).toHaveBeenCalledTimes(1); 67 }); 68 }); 69 70 describe('unsuccessful request:', () => { 71 let fakeSocket: WebSocket; 72 73 beforeEach(() => { 74 fakeSocket = UnitTestUtils.makeFakeWebSocket(); 75 }); 76 77 it('not found', async () => { 78 devicesStreamSpy.and.callFake((_, dataListener, errorListener) => { 79 return new DevicesStream(fakeSocket, dataListener, errorListener); 80 }); 81 await connection.requestDevices(); 82 fakeSocket.onerror!(new Event('error')); 83 expect(popupSpy).not.toHaveBeenCalled(); 84 expect(listener.onConnectionStateChange).toHaveBeenCalledOnceWith( 85 ConnectionState.NOT_FOUND, 86 ); 87 listener.onConnectionStateChange.calls.reset(); 88 }); 89 90 it('unauthorized server - pop ups enabled', async () => { 91 await requestDevicesFromUnauthServer(); 92 expect(popupSpy).toHaveBeenCalledOnceWith(testApproveUrl); 93 expect(listener.onConnectionStateChange).toHaveBeenCalledWith( 94 ConnectionState.UNAUTH, 95 ); 96 listener.onConnectionStateChange.calls.reset(); 97 }); 98 99 it('unauthorized server - pop ups disabled', async () => { 100 popupSpy.and.returnValue(false); 101 await requestDevicesFromUnauthServer(); 102 expect(listener.onError).toHaveBeenCalledWith( 103 'Please enable popups and try again.', 104 ); 105 listener.onError.calls.reset(); 106 }); 107 108 it('unauthorized server - force shows pop up multiple times', async () => { 109 devicesStreamSpy.calls.reset(); 110 await requestDevicesFromUnauthServer(); 111 await requestDevicesFromUnauthServer(); 112 expect(popupSpy.calls.allArgs()).toEqual([ 113 [testApproveUrl], 114 [testApproveUrl], 115 ]); 116 listener.onConnectionStateChange.calls.reset(); 117 }); 118 119 it('error - no approve URL, message present', async () => { 120 await requestDevices({error: {message: 'test message'}}, fakeSocket); 121 expect(listener.onError).toHaveBeenCalledWith('test message'); 122 expect(popupSpy).not.toHaveBeenCalled(); 123 listener.onError.calls.reset(); 124 }); 125 126 it('error - no approve URL or message', async () => { 127 await requestDevices({error: {}}, fakeSocket); 128 expect(listener.onError).toHaveBeenCalledWith('Unknown WDP Error'); 129 expect(popupSpy).not.toHaveBeenCalled(); 130 listener.onError.calls.reset(); 131 }); 132 133 it('set security token does not throw', () => { 134 expect(() => connection.setSecurityToken('')).not.toThrow(); 135 }); 136 137 async function requestDevicesFromUnauthServer() { 138 await requestDevices( 139 { 140 error: { 141 type: 'ORIGIN_NOT_ALLOWLISTED', 142 approveUrl: testApproveUrl, 143 }, 144 }, 145 fakeSocket, 146 ); 147 } 148 }); 149 150 describe('device requests:', () => { 151 let fakeDevicesSocket: jasmine.SpyObj<WebSocket>; 152 const testApproveDeviceUrl = 'test_approve_device_url'; 153 const mockDevJson: WdpDeviceConnectionResponse = { 154 serialNumber: '35562', 155 proxyStatus: 'ADB', 156 adbStatus: 'DEVICE', 157 adbProps: { 158 model: 'Pixel 6', 159 }, 160 approveUrl: testApproveDeviceUrl, 161 }; 162 163 beforeEach(() => { 164 fakeDevicesSocket = UnitTestUtils.makeFakeWebSocket(); 165 spyOn(WdpDeviceConnection.prototype, 'updateProperties'); 166 }); 167 168 it('handles empty response', async () => { 169 await requestDevices({}, fakeDevicesSocket); 170 checkDevices([]); 171 }); 172 173 it('handles empty devices in response', async () => { 174 await requestDevices({device: []}, fakeDevicesSocket); 175 checkDevices([]); 176 }); 177 178 it('adds new device', async () => { 179 await requestDevices({device: [mockDevJson]}, fakeDevicesSocket); 180 checkDevices([ 181 new WdpDeviceConnection( 182 mockDevJson.serialNumber, 183 listener, 184 testApproveDeviceUrl, 185 ), 186 ]); 187 }); 188 189 it('removes previous devices no longer present', async () => { 190 await requestDevices({device: [mockDevJson]}, fakeDevicesSocket); 191 await requestDevices({device: []}, fakeDevicesSocket); 192 checkDevices([]); 193 }); 194 195 function checkDevices(expectedDevices: WdpDeviceConnection[], popups = 0) { 196 expect(listener.onConnectionStateChange.calls.mostRecent().args).toEqual([ 197 ConnectionState.IDLE, 198 ]); 199 expect(listener.onDevicesChange.calls.mostRecent().args).toEqual([ 200 expectedDevices, 201 ]); 202 expect(connection.getDevices()).toEqual(expectedDevices); 203 expect(popupSpy).toHaveBeenCalledTimes(popups); 204 listener.onConnectionStateChange.calls.reset(); 205 listener.onDevicesChange.calls.reset(); 206 } 207 }); 208 209 async function requestDevices( 210 response: WdpRequestDevicesResponse, 211 fakeSocket: WebSocket, 212 ) { 213 await new Promise<void>((resolve) => { 214 devicesStreamSpy.and.callFake((_, dListener, eListener) => { 215 const newDataListener = async (data: string) => { 216 await dListener(data); 217 resolve(); 218 }; 219 return new DevicesStream(fakeSocket, newDataListener, eListener); 220 }); 221 const data = JSON.stringify(response); 222 connection.requestDevices().then(() => { 223 const message = UnitTestUtils.makeFakeWebSocketMessage(data); 224 fakeSocket.onmessage!(message); 225 }); 226 }); 227 } 228 229 function resetListener() { 230 listener.onAvailableTracesChange.calls.reset(); 231 listener.onError.calls.reset(); 232 listener.onConnectionStateChange.calls.reset(); 233 listener.onDevicesChange.calls.reset(); 234 } 235}); 236