Skip to content

Latest commit

 

History

History
457 lines (370 loc) · 20.6 KB

File metadata and controls

457 lines (370 loc) · 20.6 KB

Appium interceptor commands

To enable network interception, configure your Appium session using the appium:startProxyAutomatically capability. Depending on your configuration, you can manage the proxy and mocking as follows:

🔸 Automatic Mode: Set appium:startProxyAutomatically to true in your desired capabilities. The plugin will immediately initialize the proxy and configure the device upon session start.

🔸 Manual Mode: If the capability is set to false (default), you must explicitly trigger the proxy initialization using the startProxy command during your test.

👉 Once the proxy is successfully started (either automatically or manually), you can manage API mocking, recording, and sniffing using the commands detailed below.

To route emulator traffic through another proxy, set one of the environment variables UPSTREAM_PROXY, HTTPS_PROXY, or HTTP_PROXY. All traffic from the emulator will then be forwarded to the specified upstream proxy.

Mock Configuration

Mock configuration is a json object that defines the specification for filtering and applying various updates to the api request and below is the structure for the config object.

{
  url: string;
  method?: string;
  updateUrl?: [{ regexp: string, value: string}];
  headers?: object  | { add: object : string, remove: string[]};
  requestBody?: string;
  updateRequestBody?: [{regexp: string, value: string }] | [{jsonPath: string, value: string}];
  statusCode?: number;
  responseHeaders?: object  | { add: object : string, remove: string[]};
  responseBody?: string;
  updateResponseBody?: [{regexp: string, value: string }] | [{jsonPath: string, value: string}];
}
Name Type Required Description Example
url string yes Regular Expression or Glob pattern matcher to filter the request for applying the mock Sample url : https://www.reqres.in/api/users?page=1
Regex example: /api/users?.*/g
Glob pattern: **/api/users*?*
method string no Method to matching the request for applying the mock GET / POST / PUT / PATCH
updateUrl string no Regular Expression patter and replaceable value to update the outgoing url Sample url : https://www.reqres.in/api/users?page=1
When passing { updateUrl : {regexp: "/page=(\\d)+/g", value: "page=5"} }
Then outgoing url will be replaced to https://www.reqres.in/api/users?page=2
headers object no Map of key value pairs to be added or removed from the request header 1. Passing a plain object map will add all the key: value pairs to the request headers {headers : {"Content-Type" : "application/json", "Authorization": "Bearer sometoken"} }
2. If you want to add and remove header values simulaneously then you can pass the header as {headers : { add : {"Content-Type" : "application/json"}, remove: ["Authorization", "X-Content-Type"] } }
requestBody string no This will replace the original payload (post body) send to api the from the application and updates it with new body When passing {"url" : "/api/login/g" , "requestBody": "{\"email\": \"invalid@email.com\", \"password\": \"wrongpassword\"}"} will send the given payload for all login api calls made from the application
updateRequestBody string no This is similar to requestBody but instead of fully replacing the request payload, you can replace any value in the payload using Regular expression or jsonpath Consider you application sending {\"username\": \"someusername\", \"email\": \"someemail@email.com\", \"password\": \"somepassword\", \"isAdmin\" : \"false\"} as a payload for a register user api request and you want to update the email and username, then you can pass
{"updateRequestBody": [{ "jsonPath": "$.email", "newemail@email.com" }, { "jsonPath": "$.username", "new_username" }]} and it will update the email and username field before sending the request to the server
statusCode number no Updates the response status code with the given value To simulate any unexpected error you can send some of the below statusCode
1. 500 - Internal server error
2. 400 - Bad request
3. 401 - Unauthorized
responseHeaders object no Map of key value pairs to be added or removed from the response header Same syntax as headers key. But this will update the response header
responseBody object no This will replace the original response data returned by the api server and updates it with new data Passing the config as {"url" : "/api/login/g" , "responseBody": "{\"error\": \"User account locked\"}", statusCode: 400 } will simulate a error scenario when logged in with any user credentilas
updateResponseBody string no This is similar to responseBody but instead of fully mocking the server response, you can replace any value in the response using Regular expression or jsonpath Consider you application returns user data as {\"username\": \"someusername\", \"email\": \"someemail@email.com\", \"isAdmin\" : \"false\"} as a response for get user api request and you want to update the values for email and IsAdmin fiels, then you can pass
{"updateRequestBody": [{ "jsonPath": "$.email", value: "newemail@email.com" }, { "jsonPath": "$.isAdmin", value: "true" }]} and it will update the email and isAdmin field before sending the response back to the application

Commands:

interceptor: startProxy / stopProxy

These commands allow you to manually manage the proxy lifecycle during a test session. This is the preferred method when appium:startProxyAutomatically is set to false, or if you need to reset the network configuration on-the-fly.

Note: Even when using these manual commands, the plugin provides smart cleanup: it will automatically stop the proxy and restore your previous device settings when the session ends or crashes.

Example:

Note: Below example uses wedriver.io javascript client. For Java client you need to use ((JavascriptExecutor) driver).executeScript() for executing commands instead of driver.execute()

// Manually starting the proxy
await driver.execute("interceptor: startProxy");

// ... perform your intercepted tests ...

// Manually stopping the proxy
// This will revert your device WiFi settings to their original state
await driver.execute("interceptor: stopProxy");

interceptor: addMock

Add a new mock specification for intercepting and updating the request. The command will returns a unique id for each mock which can be used in future to delete the mock at any point in the test.

Example:

Note: Below example uses wedriver.io javascript client. For Java client you need to use ((JavascriptExecutor) driver).executeScript() for executing commands instead of driver.execute()

 const authorizationMock = await driver.execute("interceptor: addMock", {
    config: {
        url: "**/reqres.in/**",
        headers: {
            "Authorization" : "Bearer bearertoken"
        }
    }
 });

  const userListGetMock = await driver.execute("interceptor: addMock", {
    config: {
        url: "**/reqres.in/api/users",
        method: "GET",
        responseBody: JSON.stringify({
            page: 2,
            count: 2,
            data: [
                {
                    "first_name" : "User",
                    "last_name" : "1"
                 },
                 {
                     "first_name" : "User",
                     "last_name" : "2"
                }
            ]
        })
    }
 });

authorizationMock will be executed for all api calls made to reqres.in domain and userListGetMock will be applied for https://www.reqres.in/api/users with GET http method.

interceptor: removeMock

Given a mockId return during addMock command, will remove the mock configuration from the proxy sever.

Example:

 const authorizationMock = await driver.execute("interceptor: addMock", {
    config: {
        url: "**/reqres.in/**",
        headers: {
            "Authorization" : "Bearer bearertoken"
        }
    }
 });

 //peform user action
 //perform validation
 ..
 ..

 await driver.execute("interceptor: removeMock", {
    id: authorizationMock
 });

 // authorizationMock will not be active after this point and the test will proceed with normal flow

interceptor: startListening

Start listening for all network traffic (API calls) made by the device during a session

Example:

  await driver.execute("interceptor: startListening");
  // perform some action
  // ...

It also supports filtering the request based on the url. include will only listents for requests that macthes the given url pattern and exclude will listen for all api's that doesn't match the url pattern.

  await driver.execute("interceptor: startListening", {
    config: {
      include : [{
        url: "**/reqres.in/**",
      }]
    }
 });
  // perform some action
  // ...
  await driver.execute("interceptor: startListening", {
    config: {
      exclude : [{
        url: "**/reqres.in/**",
      }]
    }
 });
  // perform some action
  // ...

interceptor: getInterceptedData

Return all previously recorded api calls.

Example:

  await driver.execute("interceptor: startListening");
  // perform some action
  // ...
  const apiRequests = await driver.execute("interceptor: getInterceptedData");

Returns:

getInterceptedData command will return an array of network details in the below JSON format

[
  {
      "requestBody": "",
      "requestHeaders": {
        "host": "reqres.in",
        "connection": "keep-alive",
        "content-length": "41",
        "sec-ch-ua": "\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\"",
        "sec-ch-ua-mobile": "?1",
        "user-agent": "Mozilla/5.0 (Linux; Android 12; sdk_gphone64_arm64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Mobile Safari/537.36",
        "content-type": "application/json",
        "accept": "*/*",
        "origin": "https://reqres.in",
        "sec-fetch-site": "same-origin",
        "sec-fetch-mode": "cors",
        "sec-fetch-dest": "empty",
        "referer": "https://reqres.in/",
        "accept-encoding": "gzip, deflate, br",
        "accept-language": "en-US,en;q=0.9",
        "cookie": "_gid=GA1.2.1828776619.1706164095; __stripe_mid=3d0fd295-9d68-4d75-bdb2-55b809fb49ed8dba35; __stripe_sid=466b9d3a-2d7b-4f48-9c66-ca859c8d342f06a86f; _gat=1; _gat_gtag_UA_174008107_1=1; _ga_CESXN06JTW=GS1.1.1706164096.1.1.1706166680.0.0.0; _ga=GA1.1.546181777.1706164095; _ga_WSM10MMEKC=GS1.2.1706164097.1.1.1706166681.0.0.0"
      },
      "url": "https://reqres.in/api/users/2",
      "method": "PUT",
      "responseBody": "{\"name\":\"morpheus\",\"job\":\"zion resident\",\"updatedAt\":\"2024-01-25T07:24:58.607Z\"}",
      "responseHeaders": {
        "http/1.1 200 ok": "HTTP/1.1 200 OK",
        "date": "Thu, 25 Jan 2024 07:24:58 GMT",
        "content-type": "application/json; charset=utf-8",
        "transfer-encoding": "chunked",
        "connection": "close",
        "report-to": "{\"group\":\"heroku-nel\",\"max_age\":3600,\"endpoints\":[{\"url\":\"https://nel.heroku.com/reports?ts=1706167498&sid=c4c9725f-1ab0-44d8-820f-430df2718e11&s=OTZf6wqjMxJtlD7uxpJC1eBUfbrlcO7RrUKeTeefoG0%3D\"}]}",
        "reporting-endpoints": "heroku-nel=https://nel.heroku.com/reports?ts=1706167498&sid=c4c9725f-1ab0-44d8-820f-430df2718e11&s=OTZf6wqjMxJtlD7uxpJC1eBUfbrlcO7RrUKeTeefoG0%3D",
        "nel": "{\"report_to\":\"heroku-nel\",\"max_age\":3600,\"success_fraction\":0.005,\"failure_fraction\":0.05,\"response_headers\":[\"Via\"]}",
        "x-powered-by": "Express",
        "access-control-allow-origin": "*",
        "etag": "W/\"50-XmcMaub9BFf/y9879X3p35X0L4c\"",
        "via": "1.1 vegur",
        "cf-cache-status": "DYNAMIC",
        "vary": "Accept-Encoding",
        "server": "cloudflare",
        "cf-ray": "84aec7909fed601c-SIN"
      },
      "statusCode": 200
    },
    {
      "requestBody": "",
      "requestHeaders": {
        "host": "reqres.in",
        "connection": "keep-alive",
        "sec-ch-ua": "\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\"",
        "sec-ch-ua-mobile": "?1",
        "user-agent": "Mozilla/5.0 (Linux; Android 12; sdk_gphone64_arm64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Mobile Safari/537.36",
        "content-type": "application/json",
        "accept": "*/*",
        "origin": "https://reqres.in",
        "sec-fetch-site": "same-origin",
        "sec-fetch-mode": "cors",
        "sec-fetch-dest": "empty",
        "referer": "https://reqres.in/",
        "accept-encoding": "gzip, deflate, br",
        "accept-language": "en-US,en;q=0.9",
        "cookie": "_gid=GA1.2.1828776619.1706164095; __stripe_mid=3d0fd295-9d68-4d75-bdb2-55b809fb49ed8dba35; __stripe_sid=466b9d3a-2d7b-4f48-9c66-ca859c8d342f06a86f; _gat=1; _gat_gtag_UA_174008107_1=1; _ga_CESXN06JTW=GS1.1.1706164096.1.1.1706166680.0.0.0; _ga=GA1.1.546181777.1706164095; _ga_WSM10MMEKC=GS1.2.1706164097.1.1.1706166681.0.0.0"
      },
      "url": "https://reqres.in/api/users/2",
      "method": "DELETE",
      "responseBody": "",
      "responseHeaders": {
        "http/1.1 204 no content": "HTTP/1.1 204 No Content",
        "date": "Thu, 25 Jan 2024 07:24:59 GMT",
        "content-length": "0",
        "connection": "close",
        "report-to": "{\"group\":\"heroku-nel\",\"max_age\":3600,\"endpoints\":[{\"url\":\"https://nel.heroku.com/reports?ts=1706167499&sid=c4c9725f-1ab0-44d8-820f-430df2718e11&s=GzTutDCgQC4QQ%2BomNat%2BqJScD%2BtwfgViqmG7fz6%2F9yk%3D\"}]}",
        "reporting-endpoints": "heroku-nel=https://nel.heroku.com/reports?ts=1706167499&sid=c4c9725f-1ab0-44d8-820f-430df2718e11&s=GzTutDCgQC4QQ%2BomNat%2BqJScD%2BtwfgViqmG7fz6%2F9yk%3D",
        "nel": "{\"report_to\":\"heroku-nel\",\"max_age\":3600,\"success_fraction\":0.005,\"failure_fraction\":0.05,\"response_headers\":[\"Via\"]}",
        "x-powered-by": "Express",
        "access-control-allow-origin": "*",
        "etag": "W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\"",
        "via": "1.1 vegur",
        "cf-cache-status": "DYNAMIC",
        "server": "cloudflare",
        "cf-ray": "84aec7977f6c9f77-SIN"
      },
      "statusCode": 204
    }
]

interceptor: stopListening

Stops listening for networks traffic and return all previously recorded api calls.

Example:

  await driver.execute("interceptor: startListening");
  // perform some action
  // ...
  const apiRequests = await driver.execute("interceptor: stopListening");

Returns:

stopListening command will return an array of network details in the same format than getInterceptedData.

Java Example

Map<String, String> config = new HashMap();
config.put("url", "/api/users?.*");
config.put("responseBody","{\n" +
                        "  \"page\": 2,\n" +
                        "  \"per_page\": 6,\n" +
                        "  \"total\": 12,\n" +
                        "  \"total_pages\": 2,\n" +
                        "  \"data\": [\n" +
                        "    {\n" +
                        "      \"id\": 7,\n" +
                        "      \"email\": \"michael.test@reqres.in\",\n" +
                        "      \"first_name\": \"Michael\",\n" +
                        "      \"last_name\": \"Lawson\",\n" +
                        "      \"avatar\": \"https://reqres.in/img/faces/7-image.jpg\"\n" +
                        "    }\n" +
                        "  ]\n" +
                        "}");
((JavascriptExecutor)DriverManager.getDriver()).executeScript("interceptor: addMock", new HashMap(){{put("config", config); }});
driver.findElement(By.xpath("//android.widget.TextView[contains(@text,'List')]")).click();

Add or Remove Request Headers

    // Create the remove headers list
    List<String> removeHeaders = new ArrayList<>();
    removeHeaders.add("cookie");

    // Create the headers object
    Map headers = new HashMap();
     headers.put("remove", removeHeaders);

    infoLogs("** add mock code here **");
    Map<String, Object> config = new HashMap();
    config.put("url", "**/api/lobbyApi/v1/getMatches");
    config.put("headers",headers);
    Object id= ((JavascriptExecutor) driver).executeScript("interceptor: addMock", new HashMap() {{
        put("config", config);
    }});

Update request Payload partially

 ObjectMapper objectMapper = new ObjectMapper();

        ArrayNode updateRequestBodyArray = objectMapper.createArrayNode();
        // Update sportsType to 1,so that all the sports tab should show the same data.
        
        ObjectNode sportsType = objectMapper.createObjectNode();
        sportsType.put("jsonPath", "$.sportsType");
        sportsType.put("value", "1");
         
        updateRequestBodyArray.add(sportsType);


        List<Map<String, Object>> updateRequestBodyList = new ArrayList<>();
        for (int i = 0; i < updateRequestBodyArray.size(); i++) {
            Map<String, Object> map = objectMapper.convertValue(updateRequestBodyArray.get(i), Map.class);
            updateRequestBodyList.add(map);
        }

        Map<String, Object> config = new HashMap<>();
        config.put("url", "**/api/lobbyApi/v1/getMatches");
        config.put("updateRequestBody", updateRequestBodyList);

        infoLogs("Step 2: Add Mock Interceptor");
        Object id= ((JavascriptExecutor) driver).executeScript("interceptor: addMock", new HashMap<String, Object>() {{
            put("config", config);
        }});

Response Payload Partial Update

ArrayNode updateRequestBodyArray = objectMapper.createArrayNode();
        updateRequestBodyArray.add(createUpdateBodySpec
                ("$.matches.1[0].team1.dName", "Appium"));
        updateRequestBodyArray.add(createUpdateBodySpec
                ("$.matches.1[0].team2.dName", "Selenium"));
        updateRequestBodyArray.add(createUpdateBodySpec
                ("$.matches.1[0].seriesName", "The Official Appium Conference 2024"));
        

        List<Map<String, Object>> updateRequestBodyList = new ArrayList<>();
        for (int i = 0; i < updateRequestBodyArray.size(); i++) {
            Map<String, Object> map = objectMapper.convertValue(updateRequestBodyArray.get(i), Map.class);
            updateRequestBodyList.add(map);
        }
         
        Map<String, Object> config = new HashMap<>();
        config.put("url", "**/api/lobbyApi/v1/getMatches");
        config.put("updateResponseBody", updateRequestBodyList);

        Object id=   ((JavascriptExecutor) driver).executeScript("interceptor: addMock", new HashMap<String, Object>() {{
            put("config", config);
        }});

Status code update

  Map<String, Object> config = new HashMap();
        config.put("url", "**/api/fl/auth/v3/getOtp");
        config.put("statusCode", Integer.valueOf(500));

        Object id= ((JavascriptExecutor) driver)
                .executeScript("interceptor: addMock",
                        new HashMap() {{put("config", config); }});

Remove Mock


 infoLogs("** Remove the Mock **");
        ((JavascriptExecutor) driver)
                .executeScript("interceptor: removeMock", new HashMap() {{
            put("id", id);
        }});

Below code shows how to add the mock and get the id

 Object id= ((JavascriptExecutor) driver)
                .executeScript("interceptor: addMock",
                        new HashMap() {{put("config", config); }});

interceptor: getProxyState

Returns the current health and configuration state of both the interceptor proxy and the ADB device under test. This is useful for diagnostic purposes to verify if the proxy is running correctly and if the device is properly connected via ADB reverse tunnels. It can be used to monitor proxy state on client side.

Example:

  const state = await driver.execute("interceptor: getProxyState");
  console.log(JSON.parse(state));

Returns:

getProxyState will return a JSON string containing details about the proxy server and the ADB device status.

{
  "proxyServerStatus": {
    "isRegistered": true, // Indicates if the proxy exists in the internal cache
    "isStarted": true, // Indicates if the proxy server is currently running
    "deviceUDID": "R5CY127XBWB",
    "sessionId": "8e902882-ce19-44f6-90ca-975b167f94ca",
    "certificatePath": "/var/folders/.../8e902882-ce19-44f6-90ca-975b167f94ca",
    "port": 59046,
    "ip": "localhost"
  },
  "adbDeviceStatus": {
    "udid": "R5CY127XBWB",
    "activeAdbReverseTunnels": "UsbFfs tcp:56982 tcp:56982" // Current active reverse tunnels on the device
  }
}