Skip to content

Commit cca6268

Browse files
authored
feat: use own User Agent (#1779)
* refer to com.google.common.net.HttpHeaders * tweak the logic to include UA * use parameterized test * use parameterized more, defined const * modify the logic to customize the UA, add tests * match as case insensitive * tweak, fixed addHeader not to add headers twice * add annotation * define a method to build UA
1 parent c7dcb1b commit cca6268

File tree

8 files changed

+186
-11
lines changed

8 files changed

+186
-11
lines changed

build.gradle

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ java {
2525

2626
ext {
2727
seleniumVersion = project.property('selenium.version')
28+
appiumClientVersion = project.property('appiumClient.version')
2829
}
2930

3031
dependencies {
@@ -110,7 +111,7 @@ publishing {
110111
mavenJava(MavenPublication) {
111112
groupId = 'io.appium'
112113
artifactId = 'java-client'
113-
version = '8.2.0'
114+
version = appiumClientVersion
114115
from components.java
115116
pom {
116117
name = 'java-client'
@@ -186,7 +187,8 @@ wrapper {
186187

187188
processResources {
188189
filter ReplaceTokens, tokens: [
189-
'selenium.version': seleniumVersion
190+
'selenium.version': seleniumVersion,
191+
'appiumClient.version': appiumClientVersion
190192
]
191193
}
192194

gradle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ ossrhUsername=your-jira-id
88
ossrhPassword=your-jira-password
99

1010
selenium.version=4.5.0
11+
# Please increment the value in a release
12+
appiumClient.version=8.2.0

src/main/java/io/appium/java_client/AppiumClientConfig.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import org.openqa.selenium.Credentials;
2020
import org.openqa.selenium.internal.Require;
21-
import org.openqa.selenium.remote.http.AddSeleniumUserAgent;
2221
import org.openqa.selenium.remote.http.ClientConfig;
2322
import org.openqa.selenium.remote.http.Filter;
2423

@@ -34,7 +33,7 @@
3433
public class AppiumClientConfig extends ClientConfig {
3534
private final boolean directConnect;
3635

37-
private static final Filter DEFAULT_FILTER = new AddSeleniumUserAgent();
36+
private static final Filter DEFAULT_FILTER = new AppiumUserAgentFilter();
3837

3938
private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMinutes(10);
4039

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* See the NOTICE file distributed with this work for additional
5+
* information regarding copyright ownership.
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+
17+
package io.appium.java_client;
18+
19+
import com.google.common.annotations.VisibleForTesting;
20+
import com.google.common.net.HttpHeaders;
21+
import io.appium.java_client.internal.Config;
22+
import org.openqa.selenium.remote.http.AddSeleniumUserAgent;
23+
import org.openqa.selenium.remote.http.Filter;
24+
import org.openqa.selenium.remote.http.HttpHandler;
25+
26+
import javax.annotation.Nonnull;
27+
import javax.annotation.Nullable;
28+
29+
/**
30+
* Manage Appium Client configurations.
31+
*/
32+
33+
public class AppiumUserAgentFilter implements Filter {
34+
35+
public static final String VERSION_KEY = "appiumClient.version";
36+
37+
private static final String USER_AGENT_PREFIX = "appium/";
38+
39+
/**
40+
* A default User Agent name for Appium Java client.
41+
* e.g. appium/8.2.0 (selenium/4.5.0 (java mac))
42+
*/
43+
public static final String USER_AGENT = buildUserAgentHeaderValue(AddSeleniumUserAgent.USER_AGENT);
44+
45+
private static String buildUserAgentHeaderValue(@Nonnull String previousUA) {
46+
return String.format("%s%s (%s)",
47+
USER_AGENT_PREFIX, Config.main().getValue(VERSION_KEY, String.class), previousUA);
48+
}
49+
50+
/**
51+
* Returns true if the given User Agent includes "appium/", which
52+
* implies the User Agent already has the Appium UA by this method.
53+
* The matching is case-insensitive.
54+
* @param userAgent the User Agent in the request headers.
55+
* @return whether the given User Agent includes Appium UA
56+
* like by this filter.
57+
*/
58+
@VisibleForTesting
59+
public static boolean containsAppiumName(@Nullable String userAgent) {
60+
return userAgent != null && userAgent.toLowerCase().contains(USER_AGENT_PREFIX.toLowerCase());
61+
}
62+
63+
/**
64+
* Returns the User Agent. If the given UA already has
65+
* {@link USER_AGENT_PREFIX}, it returns the UA.
66+
* IF the given UA does not have {@link USER_AGENT_PREFIX},
67+
* it returns UA with the Appium prefix.
68+
* @param userAgent the User Agent in the request headers.
69+
* @return the User Agent for the request
70+
*/
71+
public static String buildUserAgent(@Nullable String userAgent) {
72+
if (userAgent == null) {
73+
return USER_AGENT;
74+
}
75+
76+
if (containsAppiumName(userAgent)) {
77+
return userAgent;
78+
}
79+
80+
return buildUserAgentHeaderValue(userAgent);
81+
}
82+
83+
@Override
84+
public HttpHandler apply(HttpHandler next) {
85+
86+
return req -> {
87+
req.setHeader(HttpHeaders.USER_AGENT, buildUserAgent(req.getHeader(HttpHeaders.USER_AGENT)));
88+
return next.execute(req);
89+
};
90+
}
91+
}

src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import com.google.common.base.Supplier;
2525
import com.google.common.base.Throwables;
2626

27+
import com.google.common.net.HttpHeaders;
28+
import io.appium.java_client.AppiumUserAgentFilter;
2729
import io.appium.java_client.AppiumClientConfig;
2830
import org.openqa.selenium.SessionNotCreatedException;
2931
import org.openqa.selenium.WebDriverException;
@@ -192,6 +194,8 @@ private Response createSession(Command command) throws IOException {
192194

193195
ProtocolHandshake.Result result = new AppiumProtocolHandshake().createSession(
194196
getClient().with((httpHandler) -> (req) -> {
197+
req.setHeader(HttpHeaders.USER_AGENT,
198+
AppiumUserAgentFilter.buildUserAgent(req.getHeader(HttpHeaders.USER_AGENT)));
195199
req.setHeader(IDEMPOTENCY_KEY_HEADER, UUID.randomUUID().toString().toLowerCase());
196200
return httpHandler.execute(req);
197201
}), command

src/main/resources/main.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
selenium.version=@selenium.version@
2+
appiumClient.version=@appiumClient.version@
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.appium.java_client.internal;
2+
3+
import io.appium.java_client.AppiumUserAgentFilter;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.params.ParameterizedTest;
6+
import org.junit.jupiter.params.provider.Arguments;
7+
import org.junit.jupiter.params.provider.MethodSource;
8+
9+
import java.util.stream.Stream;
10+
11+
import static org.junit.jupiter.api.Assertions.*;
12+
13+
public class AppiumUserAgentFilterTest {
14+
@Test
15+
void validateUserAgent() {
16+
assertTrue(AppiumUserAgentFilter.USER_AGENT.startsWith("appium/"));
17+
}
18+
19+
public static Stream<Arguments> userAgentParams() {
20+
return Stream.of(
21+
Arguments.of("selenium/4.5.0 (java mac)", false),
22+
Arguments.of("appium/8.2.0 (selenium/4.5.0 (java mac))", true),
23+
Arguments.of("APPIUM/8.2.0 (selenium/4.5.0 (java mac))", true),
24+
Arguments.of("something (Appium/8.2.0 (selenium/4.5.0 (java mac)))", true),
25+
Arguments.of("something (appium/8.2.0 (selenium/4.5.0 (java mac)))", true)
26+
);
27+
}
28+
29+
@ParameterizedTest
30+
@MethodSource("userAgentParams")
31+
void validUserAgentIfContainsAppiumName(String userAgent, boolean expected) {
32+
assertEquals(AppiumUserAgentFilter.containsAppiumName(userAgent), expected);
33+
}
34+
35+
@Test
36+
void validBuildUserAgentNoUA() {
37+
assertEquals(AppiumUserAgentFilter.buildUserAgent(null), AppiumUserAgentFilter.USER_AGENT);
38+
}
39+
40+
@Test
41+
void validBuildUserAgentNoAppium1() {
42+
String ua = AppiumUserAgentFilter.buildUserAgent("selenium/4.5.0 (java mac)");
43+
assertTrue(ua.startsWith("appium/"));
44+
assertTrue(ua.endsWith("selenium/4.5.0 (java mac))"));
45+
}
46+
47+
@Test
48+
void validBuildUserAgentNoAppium2() {
49+
String ua = AppiumUserAgentFilter.buildUserAgent("customSelenium/4.5.0 (java mac)");
50+
assertTrue(ua.startsWith("appium/"));
51+
assertTrue(ua.endsWith("customSelenium/4.5.0 (java mac))"));
52+
}
53+
54+
@Test
55+
void validBuildUserAgentAlreadyHasAppium1() {
56+
// Won't modify since the UA already has appium prefix
57+
String ua = AppiumUserAgentFilter.buildUserAgent("appium/8.1.0 (selenium/4.5.0 (java mac))");
58+
assertEquals("appium/8.1.0 (selenium/4.5.0 (java mac))", ua);
59+
}
60+
61+
@Test
62+
void validBuildUserAgentAlreadyHasAppium2() {
63+
// Won't modify since the UA already has appium prefix
64+
String ua = AppiumUserAgentFilter.buildUserAgent("something (appium/8.1.0 (selenium/4.5.0 (java mac)))");
65+
assertEquals("something (appium/8.1.0 (selenium/4.5.0 (java mac)))", ua);
66+
}
67+
}

src/test/java/io/appium/java_client/internal/ConfigTest.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,36 @@
66
import static org.junit.jupiter.api.Assertions.assertThrows;
77
import static org.junit.jupiter.api.Assertions.assertTrue;
88

9+
import io.appium.java_client.AppiumUserAgentFilter;
910
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.Arguments;
13+
import org.junit.jupiter.params.provider.MethodSource;
14+
import org.junit.jupiter.params.provider.ValueSource;
15+
16+
import java.util.stream.Stream;
1017

1118
class ConfigTest {
1219
private static final String SELENIUM_EXISTING_KEY = "selenium.version";
1320

1421
private static final String MISSING_KEY = "bla";
1522

16-
@Test
17-
void verifyGettingExistingValue() {
18-
assertThat(Config.main().getValue(SELENIUM_EXISTING_KEY, String.class).length(), greaterThan(0));
19-
assertTrue(Config.main().getOptionalValue(SELENIUM_EXISTING_KEY, String.class).isPresent());
23+
@ParameterizedTest
24+
@ValueSource(strings = {SELENIUM_EXISTING_KEY, AppiumUserAgentFilter.VERSION_KEY})
25+
void verifyGettingExistingValue(String key) {
26+
assertThat(Config.main().getValue(key, String.class).length(), greaterThan(0));
27+
assertTrue(Config.main().getOptionalValue(key, String.class).isPresent());
2028
}
2129

2230
@Test
2331
void verifyGettingNonExistingValue() {
2432
assertThrows(IllegalArgumentException.class, () -> Config.main().getValue(MISSING_KEY, String.class));
2533
}
2634

27-
@Test
28-
void verifyGettingExistingValueWithWrongClass() {
29-
assertThrows(ClassCastException.class, () -> Config.main().getValue(SELENIUM_EXISTING_KEY, Integer.class));
35+
@ParameterizedTest
36+
@ValueSource(strings = {SELENIUM_EXISTING_KEY, AppiumUserAgentFilter.VERSION_KEY})
37+
void verifyGettingExistingValueWithWrongClass(String key) {
38+
assertThrows(ClassCastException.class, () -> Config.main().getValue(key, Integer.class));
3039
}
3140

3241
@Test

0 commit comments

Comments
 (0)