appium-xcuitest-driver 10.15.0 → 10.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/build/lib/commands/certificate.d.ts.map +1 -1
- package/build/lib/commands/certificate.js +33 -32
- package/build/lib/commands/certificate.js.map +1 -1
- package/build/lib/device/certificate-client.d.ts +73 -0
- package/build/lib/device/certificate-client.d.ts.map +1 -0
- package/build/lib/device/certificate-client.js +163 -0
- package/build/lib/device/certificate-client.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/lib/commands/certificate.ts +41 -37
- package/lib/device/certificate-client.ts +195 -0
- package/npm-shrinkwrap.json +5 -5
- package/package.json +1 -1
|
@@ -7,8 +7,8 @@ import path from 'path';
|
|
|
7
7
|
import http from 'http';
|
|
8
8
|
import {exec} from 'teen_process';
|
|
9
9
|
import {findAPortNotInUse, checkPortStatus} from 'portscanner';
|
|
10
|
-
import {
|
|
11
|
-
import {requireRealDevice} from '../utils';
|
|
10
|
+
import {CertificateClient} from '../device/certificate-client';
|
|
11
|
+
import {requireRealDevice, isIos18OrNewer} from '../utils';
|
|
12
12
|
import type {Simulator} from 'appium-ios-simulator';
|
|
13
13
|
import type {XCUITestDriver} from '../driver';
|
|
14
14
|
import type {CertificateList} from './types';
|
|
@@ -119,21 +119,14 @@ export async function mobileInstallCertificate(
|
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
121
|
} else {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
udid: this.opts.udid,
|
|
127
|
-
log: this.log,
|
|
128
|
-
});
|
|
129
|
-
if (await client.assertExists(false)) {
|
|
130
|
-
await client.installProfile({payload: Buffer.from(content, 'base64')});
|
|
122
|
+
try {
|
|
123
|
+
await withCertificateClient(this, async (client) => {
|
|
124
|
+
await client.installCertificate({payload: Buffer.from(content, 'base64')});
|
|
125
|
+
});
|
|
131
126
|
return;
|
|
132
|
-
}
|
|
133
|
-
this.log.
|
|
134
|
-
|
|
135
|
-
'Falling back to the (slow) UI-based installation',
|
|
136
|
-
);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
this.log.error(`Failed to install the certificate: ${err.message}`);
|
|
129
|
+
this.log.info('Falling back to the (slow) UI-based installation');
|
|
137
130
|
}
|
|
138
131
|
}
|
|
139
132
|
|
|
@@ -161,7 +154,7 @@ export async function mobileInstallCertificate(
|
|
|
161
154
|
try {
|
|
162
155
|
const host = os.hostname();
|
|
163
156
|
const certUrl = `http://${host}:${tmpPort}/${configName}`;
|
|
164
|
-
|
|
157
|
+
tmpServer.listen(tmpPort);
|
|
165
158
|
try {
|
|
166
159
|
await waitForCondition(
|
|
167
160
|
async () => {
|
|
@@ -233,7 +226,7 @@ export async function mobileInstallCertificate(
|
|
|
233
226
|
|
|
234
227
|
return (await util.toInMemoryBase64(configPath)).toString();
|
|
235
228
|
} finally {
|
|
236
|
-
|
|
229
|
+
tmpServer.close();
|
|
237
230
|
await fs.rimraf(tmpRoot);
|
|
238
231
|
}
|
|
239
232
|
}
|
|
@@ -253,15 +246,7 @@ export async function mobileInstallCertificate(
|
|
|
253
246
|
*/
|
|
254
247
|
export async function mobileRemoveCertificate(this: XCUITestDriver, name: string): Promise<string> {
|
|
255
248
|
requireRealDevice(this, 'Removing certificate');
|
|
256
|
-
|
|
257
|
-
throw new Error('udid capability is required');
|
|
258
|
-
}
|
|
259
|
-
const client = new Pyidevice({
|
|
260
|
-
udid: this.opts.udid,
|
|
261
|
-
log: this.log,
|
|
262
|
-
});
|
|
263
|
-
await client.assertExists(true);
|
|
264
|
-
return await client.removeProfile(name);
|
|
249
|
+
return await withCertificateClient(this, async (client) => client.removeCertificate(name));
|
|
265
250
|
}
|
|
266
251
|
|
|
267
252
|
/**
|
|
@@ -275,17 +260,37 @@ export async function mobileRemoveCertificate(this: XCUITestDriver, name: string
|
|
|
275
260
|
*/
|
|
276
261
|
export async function mobileListCertificates(this: XCUITestDriver): Promise<CertificateList> {
|
|
277
262
|
requireRealDevice(this, 'Listing certificates');
|
|
278
|
-
|
|
279
|
-
throw new Error('udid capability is required');
|
|
280
|
-
}
|
|
281
|
-
const client = new Pyidevice({
|
|
282
|
-
udid: this.opts.udid,
|
|
283
|
-
log: this.log,
|
|
284
|
-
});
|
|
285
|
-
await client.assertExists(true);
|
|
286
|
-
return await client.listProfiles();
|
|
263
|
+
return await withCertificateClient(this, async (client) => client.listCertificates());
|
|
287
264
|
}
|
|
288
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Helper function to create a CertificateClient, execute an operation, and ensure cleanup.
|
|
268
|
+
*
|
|
269
|
+
* @param driver - The XCUITestDriver instance
|
|
270
|
+
* @param operation - A callback function that receives the client and performs the operation
|
|
271
|
+
* @returns The result of the operation callback
|
|
272
|
+
*/
|
|
273
|
+
async function withCertificateClient<T>(
|
|
274
|
+
driver: XCUITestDriver,
|
|
275
|
+
operation: (client: CertificateClient) => Promise<T>,
|
|
276
|
+
): Promise<T> {
|
|
277
|
+
let client: CertificateClient | null = null;
|
|
278
|
+
try {
|
|
279
|
+
if (!driver.opts.udid) {
|
|
280
|
+
throw new Error('udid capability is required');
|
|
281
|
+
}
|
|
282
|
+
client = await CertificateClient.create(
|
|
283
|
+
driver.opts.udid,
|
|
284
|
+
driver.log,
|
|
285
|
+
isIos18OrNewer(driver.opts),
|
|
286
|
+
);
|
|
287
|
+
return await operation(client);
|
|
288
|
+
} finally {
|
|
289
|
+
if (client) {
|
|
290
|
+
await client.close();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
289
294
|
|
|
290
295
|
/**
|
|
291
296
|
* Extracts the common name of the certificate from the given buffer.
|
|
@@ -501,4 +506,3 @@ async function installPost122Certificate(driver: XCUITestDriver, name: string):
|
|
|
501
506
|
|
|
502
507
|
return true;
|
|
503
508
|
}
|
|
504
|
-
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type {AppiumLogger} from '@appium/types';
|
|
2
|
+
import {Pyidevice} from './clients/py-ios-device-client';
|
|
3
|
+
import {getRemoteXPCServices} from './remotexpc-utils';
|
|
4
|
+
import type {CertificateList} from '../commands/types';
|
|
5
|
+
import type {
|
|
6
|
+
MobileConfigService as RemoteXPCMobileConfigService,
|
|
7
|
+
RemoteXpcConnection,
|
|
8
|
+
} from 'appium-ios-remotexpc';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Options for installing a certificate
|
|
12
|
+
*/
|
|
13
|
+
export interface InstallCertificateOptions {
|
|
14
|
+
payload: Buffer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Unified Certificate Client
|
|
19
|
+
*
|
|
20
|
+
* Provides a unified interface for certificate operations on iOS devices,
|
|
21
|
+
* automatically handling the differences between iOS < 18 (py-ios-device)
|
|
22
|
+
* and iOS 18 and above (appium-ios-remotexpc MobileConfigService).
|
|
23
|
+
*/
|
|
24
|
+
export class CertificateClient {
|
|
25
|
+
private readonly service: RemoteXPCMobileConfigService | Pyidevice;
|
|
26
|
+
private readonly remoteXPCConnection?: RemoteXpcConnection;
|
|
27
|
+
private readonly log: AppiumLogger;
|
|
28
|
+
|
|
29
|
+
private constructor(
|
|
30
|
+
service: RemoteXPCMobileConfigService | Pyidevice,
|
|
31
|
+
log: AppiumLogger,
|
|
32
|
+
remoteXPCConnection?: RemoteXpcConnection
|
|
33
|
+
) {
|
|
34
|
+
this.service = service;
|
|
35
|
+
this.log = log;
|
|
36
|
+
this.remoteXPCConnection = remoteXPCConnection;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#region Public Methods
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a certificate client for device
|
|
43
|
+
*
|
|
44
|
+
* @param udid - Device UDID
|
|
45
|
+
* @param log - Appium logger instance
|
|
46
|
+
* @param useRemoteXPC - Whether to use remotexpc (use isIos18OrNewer(opts) to determine)
|
|
47
|
+
* @returns Certificate client instance
|
|
48
|
+
*/
|
|
49
|
+
static async create(
|
|
50
|
+
udid: string,
|
|
51
|
+
log: AppiumLogger,
|
|
52
|
+
useRemoteXPC: boolean
|
|
53
|
+
): Promise<CertificateClient> {
|
|
54
|
+
if (useRemoteXPC) {
|
|
55
|
+
const client = await CertificateClient.withRemoteXpcConnection(async () => {
|
|
56
|
+
const Services = await getRemoteXPCServices();
|
|
57
|
+
const {mobileConfigService, remoteXPC} = await Services.startMobileConfigService(udid);
|
|
58
|
+
return {
|
|
59
|
+
service: mobileConfigService,
|
|
60
|
+
connection: remoteXPC,
|
|
61
|
+
};
|
|
62
|
+
}, log);
|
|
63
|
+
if (client) {
|
|
64
|
+
return client;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fallback to py-ios-device
|
|
69
|
+
const pyideviceClient = new Pyidevice({
|
|
70
|
+
udid,
|
|
71
|
+
log,
|
|
72
|
+
});
|
|
73
|
+
await pyideviceClient.assertExists(true);
|
|
74
|
+
return new CertificateClient(pyideviceClient, log);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Install a certificate profile
|
|
79
|
+
*
|
|
80
|
+
* @param options - Installation options containing the certificate payload
|
|
81
|
+
*/
|
|
82
|
+
async installCertificate(options: InstallCertificateOptions): Promise<void> {
|
|
83
|
+
const {payload} = options;
|
|
84
|
+
|
|
85
|
+
if (this.isRemoteXPC) {
|
|
86
|
+
await this.mobileConfigService.installProfileFromBuffer(payload);
|
|
87
|
+
} else {
|
|
88
|
+
await this.pyideviceClient.installProfile({payload});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove a certificate profile by name
|
|
94
|
+
*
|
|
95
|
+
* @param name - Name of the profile to remove
|
|
96
|
+
* @returns Status message ('Acknowledged' for RemoteXPC, or pyidevice output)
|
|
97
|
+
*/
|
|
98
|
+
async removeCertificate(name: string): Promise<string> {
|
|
99
|
+
if (this.isRemoteXPC) {
|
|
100
|
+
await this.mobileConfigService.removeProfile(name);
|
|
101
|
+
return 'Acknowledged';
|
|
102
|
+
} else {
|
|
103
|
+
return await this.pyideviceClient.removeProfile(name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* List installed certificate profiles
|
|
109
|
+
*
|
|
110
|
+
* @returns List of installed certificates
|
|
111
|
+
*/
|
|
112
|
+
async listCertificates(): Promise<CertificateList> {
|
|
113
|
+
if (this.isRemoteXPC) {
|
|
114
|
+
return await this.mobileConfigService.getProfileList();
|
|
115
|
+
} else {
|
|
116
|
+
return await this.pyideviceClient.listProfiles();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Close the certificate service connection and remoteXPC connection if present
|
|
122
|
+
*/
|
|
123
|
+
async close(): Promise<void> {
|
|
124
|
+
if (this.remoteXPCConnection) {
|
|
125
|
+
try {
|
|
126
|
+
this.log.debug(`Closing remoteXPC connection`);
|
|
127
|
+
await this.remoteXPCConnection.close();
|
|
128
|
+
} catch (err: any) {
|
|
129
|
+
this.log.debug(`Error closing remoteXPC connection: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
|
|
136
|
+
//#region Private Methods
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if this client is using RemoteXPC
|
|
140
|
+
*/
|
|
141
|
+
private get isRemoteXPC(): boolean {
|
|
142
|
+
return !!this.remoteXPCConnection;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Helper to safely execute remoteXPC operations with connection cleanup
|
|
147
|
+
* @param operation - Async operation that returns service and connection
|
|
148
|
+
* @param log - Logger instance
|
|
149
|
+
* @returns CertificateClient on success, null on failure
|
|
150
|
+
*/
|
|
151
|
+
private static async withRemoteXpcConnection<T extends RemoteXPCMobileConfigService | Pyidevice>(
|
|
152
|
+
operation: () => Promise<{service: T; connection: RemoteXpcConnection}>,
|
|
153
|
+
log: AppiumLogger
|
|
154
|
+
): Promise<CertificateClient | null> {
|
|
155
|
+
let remoteXPCConnection: RemoteXpcConnection | undefined;
|
|
156
|
+
let succeeded = false;
|
|
157
|
+
try {
|
|
158
|
+
const {service, connection} = await operation();
|
|
159
|
+
remoteXPCConnection = connection;
|
|
160
|
+
const client = new CertificateClient(service, log, remoteXPCConnection);
|
|
161
|
+
succeeded = true;
|
|
162
|
+
return client;
|
|
163
|
+
} catch (err: any) {
|
|
164
|
+
log.error(
|
|
165
|
+
`Failed to create certificate client via RemoteXPC: ${err.message}, falling back to py-ios-device`
|
|
166
|
+
);
|
|
167
|
+
return null;
|
|
168
|
+
} finally {
|
|
169
|
+
// Only close connection if we failed (if succeeded, the client owns it)
|
|
170
|
+
if (remoteXPCConnection && !succeeded) {
|
|
171
|
+
try {
|
|
172
|
+
await remoteXPCConnection.close();
|
|
173
|
+
} catch {
|
|
174
|
+
// Ignore cleanup errors
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get service as RemoteXPC MobileConfigService
|
|
182
|
+
*/
|
|
183
|
+
private get mobileConfigService(): RemoteXPCMobileConfigService {
|
|
184
|
+
return this.service as RemoteXPCMobileConfigService;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get service as Pyidevice client
|
|
189
|
+
*/
|
|
190
|
+
private get pyideviceClient(): Pyidevice {
|
|
191
|
+
return this.service as Pyidevice;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
//#endregion
|
|
195
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appium-xcuitest-driver",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.16.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "appium-xcuitest-driver",
|
|
9
|
-
"version": "10.
|
|
9
|
+
"version": "10.16.1",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@appium/strongbox": "^1.0.0-rc.1",
|
|
@@ -692,9 +692,9 @@
|
|
|
692
692
|
}
|
|
693
693
|
},
|
|
694
694
|
"node_modules/appium-ios-remotexpc": {
|
|
695
|
-
"version": "0.
|
|
696
|
-
"resolved": "https://registry.npmjs.org/appium-ios-remotexpc/-/appium-ios-remotexpc-0.
|
|
697
|
-
"integrity": "sha512-
|
|
695
|
+
"version": "0.26.0",
|
|
696
|
+
"resolved": "https://registry.npmjs.org/appium-ios-remotexpc/-/appium-ios-remotexpc-0.26.0.tgz",
|
|
697
|
+
"integrity": "sha512-9c7z05u2vlwhn6OPyY60Tfug3U5LPNw1k6Se0qTRUxnGs7ttWltk+5Dn+QUy4XDRKCtogmv/azVWHBlfIz2xKg==",
|
|
698
698
|
"license": "Apache-2.0",
|
|
699
699
|
"optional": true,
|
|
700
700
|
"dependencies": {
|