appium-xcuitest-driver 10.15.0 → 10.16.0
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 +6 -0
- package/build/lib/commands/certificate.d.ts.map +1 -1
- package/build/lib/commands/certificate.js +33 -22
- 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 +46 -27
- 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';
|
|
@@ -122,18 +122,23 @@ export async function mobileInstallCertificate(
|
|
|
122
122
|
if (!this.opts.udid) {
|
|
123
123
|
throw new Error('udid capability is required');
|
|
124
124
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
} else {
|
|
133
|
-
this.log.info(
|
|
134
|
-
'pyidevice is not installed on your system. ' +
|
|
135
|
-
'Falling back to the (slow) UI-based installation',
|
|
125
|
+
|
|
126
|
+
let client: CertificateClient | null = null;
|
|
127
|
+
try {
|
|
128
|
+
client = await CertificateClient.create(
|
|
129
|
+
this.opts.udid,
|
|
130
|
+
this.log,
|
|
131
|
+
isIos18OrNewer(this.opts)
|
|
136
132
|
);
|
|
133
|
+
await client.installCertificate({payload: Buffer.from(content, 'base64')});
|
|
134
|
+
return;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this.log.error(`Failed to install the certificate: ${err.message}`);
|
|
137
|
+
this.log.info('Falling back to the (slow) UI-based installation');
|
|
138
|
+
} finally {
|
|
139
|
+
if (client) {
|
|
140
|
+
await client.close();
|
|
141
|
+
}
|
|
137
142
|
}
|
|
138
143
|
}
|
|
139
144
|
|
|
@@ -256,12 +261,20 @@ export async function mobileRemoveCertificate(this: XCUITestDriver, name: string
|
|
|
256
261
|
if (!this.opts.udid) {
|
|
257
262
|
throw new Error('udid capability is required');
|
|
258
263
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
|
|
265
|
+
let client: CertificateClient | null = null;
|
|
266
|
+
try {
|
|
267
|
+
client = await CertificateClient.create(
|
|
268
|
+
this.opts.udid,
|
|
269
|
+
this.log,
|
|
270
|
+
isIos18OrNewer(this.opts)
|
|
271
|
+
);
|
|
272
|
+
return await client.removeCertificate(name);
|
|
273
|
+
} finally {
|
|
274
|
+
if (client) {
|
|
275
|
+
await client.close();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
265
278
|
}
|
|
266
279
|
|
|
267
280
|
/**
|
|
@@ -278,14 +291,21 @@ export async function mobileListCertificates(this: XCUITestDriver): Promise<Cert
|
|
|
278
291
|
if (!this.opts.udid) {
|
|
279
292
|
throw new Error('udid capability is required');
|
|
280
293
|
}
|
|
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();
|
|
287
|
-
}
|
|
288
294
|
|
|
295
|
+
let client: CertificateClient | null = null;
|
|
296
|
+
try {
|
|
297
|
+
client = await CertificateClient.create(
|
|
298
|
+
this.opts.udid,
|
|
299
|
+
this.log,
|
|
300
|
+
isIos18OrNewer(this.opts)
|
|
301
|
+
);
|
|
302
|
+
return await client.listCertificates();
|
|
303
|
+
} finally {
|
|
304
|
+
if (client) {
|
|
305
|
+
await client.close();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
289
309
|
|
|
290
310
|
/**
|
|
291
311
|
* Extracts the common name of the certificate from the given buffer.
|
|
@@ -501,4 +521,3 @@ async function installPost122Certificate(driver: XCUITestDriver, name: string):
|
|
|
501
521
|
|
|
502
522
|
return true;
|
|
503
523
|
}
|
|
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.0",
|
|
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.0",
|
|
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": {
|