Skip to content

Manual test script

Background

MCT needs to migrate from VMware to OCP4. Consequently, the test timer has to be migrated from the VMware agent to the Kubed agent. However, this timer migration has a significant impact and cannot be fully tested prior to the migration.

For instance, some tests rely on network accessibility. We can inform customers to open ACL to grant access to Kubed, but there may be oversights. If the migration occurs, it could lead to false alert incidents.

We could conduct manual tests first for all cases planned for migration, but the quantity is too large to be done manually. Thus, we believe we can write a script to simulate this batch of manual test operations.

Script Design

Language: Python

Simulate MCT page operations for batch operations.

Manual test zone on page

Check the http request and use it to perform manual tests in the script.

Input

  • Username (the person conducting the manual test, who can then check the result on the manual test history page).
  • zoneid list (the zones to be manually tested; you can use SQL to filter the target zoneid list).

Output

  • The zoneid and ManualSessionId for each request.
  • All ManualSessionIds combined to facilitate input for the manual test check script.

Check manual test result on page

Check the http request and use it to check the manual test results in the script.

You may need to execute this script more than once as manual test execution takes some time.

Input

  • ManualSessionId list (obtainable from the manual test script output).

Output

  • Only output manual tests with a result other than "pass".
    • For each non-pass manual test, check its check's past 1 hour status with "Current Hour Status" API used in search page
  • Key information for each request, such as the check name, test result, and detail status link.
  • All check name and detail status links for non-passing tests combined.
  • All check name and detail status links for non-passing tests combined.(except for always failed check)
    • Currently, "always failed check" means that all statuses of the check have not passed in the past 1 hour.

Set up script env

It is recommended to run with a Python 3.9 virtual environment.

lewan@LEWAN-M-6FV6 manual-test-script % python3.9 -m venv venv_py39
lewan@LEWAN-M-6FV6 manual-test-script % source venv_py39/bin/activate
(venv_py39) lewan@LEWAN-M-6FV6 manual-test-script % pip install --upgrade requests

Script

Replace {ci-token} with a valid token, e.g., an MCT internal token or a token from an MCT portal request.

Manual test script

import argparse
import requests
import json

def main():
    """
    Main function to parse arguments, make API requests, and print results.
    """
    parser = argparse.ArgumentParser(description="Make API requests for manual testing with dynamic zone IDs.")
    parser.add_argument("-cec", "--cec", required=True, help="Username for the API request (e.g., 'lewan' or 'test').")
    parser.add_argument("-zoneids", "--zoneids", required=True, help="Comma-separated list of zone IDs (e.g., '1221961,1221962').")

    args = parser.parse_args()

    username_input = args.cec
    zone_ids_str = args.zoneids

    try:
        # Parse the comma-separated zone IDs into a list of integers
        zone_ids = [int(zid.strip()) for zid in zone_ids_str.split(',') if zid.strip()]
    except ValueError:
        print("Error: Zone IDs must be a comma-separated list of integers.")
        return

    if not zone_ids:
        print("Error: No valid zone IDs provided after parsing.")
        return

    api_url = "https://mctapi.prod.webex.com/mpi/test/manual/123"
    all_manual_session_ids = []

    # Base headers for the API request
    base_headers = {
        "accept": "application/json, text/plain, */*",
        "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
        "authorization": "{ci-token}",
        "content-type": "application/json; charset=UTF-8",
    }

    # Base payload for the API request. zoneId will be updated dynamically.
    base_payload = {
        "clusterId": "78791",
        "mtype": 2,
        "serverIp": "",
        "domainId": 0,
        "zoneId": 0, # This will be replaced for each request
        "serverId": 0,
        "serverType": 0,
        "protocol": 0,
        "agent": "wjfkgen-p-1",
        "identifier": "",
        "vGroupId": 1100100102
    }

    # Set the 'username' header based on the input 'cec'
    headers = base_headers.copy()
    headers["username"] = "test" if username_input.lower() == "test" else username_input

    # Iterate through each zone ID and make a request
    for i, zone_id in enumerate(zone_ids):
        print(f"request {i + 1}:")

        # Create a copy of the base payload and update the zoneId
        payload = base_payload.copy()
        payload["zoneId"] = zone_id

        try:
            # Make the POST request
            response = requests.post(api_url, headers=headers, json=payload)
            response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)

            response_data = response.json()

            # Check if the response indicates success and contains ManualSessionId
            if response_data.get("errorCode") == "OKOKOK" and "ManualSessionId" in response_data:
                manual_session_id = response_data["ManualSessionId"]
                print(f'"zoneId": {zone_id}')
                print(f'ManualSessionId": "{manual_session_id}"')
                all_manual_session_ids.append(manual_session_id)
            else:
                print(f"  API response error for zoneId {zone_id}: {response_data.get('errorCode', 'Unknown Error')}")
                print(f"  Full response: {json.dumps(response_data, indent=2)}")

        except requests.exceptions.HTTPError as err:
            print(f"  HTTP error occurred for zoneId {zone_id}: {err}")
            print(f"  Response content: {err.response.text}")
        except requests.exceptions.ConnectionError as err:
            print(f"  Connection error occurred for zoneId {zone_id}: {err}")
        except requests.exceptions.Timeout as err:
            print(f"  Timeout error occurred for zoneId {zone_id}: {err}")
        except requests.exceptions.RequestException as err:
            print(f"  An unexpected request error occurred for zoneId {zone_id}: {err}")
        except json.JSONDecodeError:
            print(f"  Failed to decode JSON response for zoneId {zone_id}. Response content: {response.text}")
        except Exception as e:
            print(f"  An unexpected error occurred for zoneId {zone_id}: {e}")
        print() # Add a newline for better readability between requests

    # Print all collected ManualSessionIds at the end
    if all_manual_session_ids:
        print(f'ManualSessionIds: "{", ".join(all_manual_session_ids)}"')
    else:
        print("No ManualSessionIds were successfully retrieved.")

if __name__ == "__main__":
    main()

Manual test result check script

import argparse
import requests
import json
import datetime
import urllib.parse

def main():
    """
    Main function to parse arguments, make API requests to get manual test results,
    and print relevant information, including historical status checks.
    """
    parser = argparse.ArgumentParser(description="Retrieve and display manual test results based on manual IDs.")
    parser.add_argument("-manualids", "--manualids", required=True,
                        help="Comma-separated list of manual IDs (e.g., '135752171,135752421').")

    args = parser.parse_args()

    manual_ids_str = args.manualids

    try:
        # Parse the comma-separated manual IDs into a list of integers
        manual_ids = [int(mid.strip()) for mid in manual_ids_str.split(',') if mid.strip()]
    except ValueError:
        print("Error: Manual IDs must be a comma-separated list of integers.")
        return

    if not manual_ids:
        print("Error: No valid manual IDs provided after parsing.")
        return

    base_manual_api_url = "https://mctapi.prod.webex.com/mpi/test/manual-id/"
    base_detail_status_url = "https://mct.webex.com/detail-status/"
    base_historical_status_api_url = "https://mctapi.prod.webex.com/mpi/clusterlist/queryZoneServerDetail"

    all_failed_detail_links_summary = [] # Stores (name, link) for all non-passing items
    new_or_intermittent_failures_summary = [] # Stores (name, link) for items where "all status not pass in last 1h: no"

    # Headers for the API requests
    headers = {
        "accept": "application/json, text/plain, */*",
        "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
        "authorization": "{ci-token}", # Include if required by the API
        "content-type": "application/json; charset=UTF-8",
    }

    for i, manual_id in enumerate(manual_ids):
        print(f"request {i + 1}:")
        manual_api_url = f"{base_manual_api_url}{manual_id}/0"

        found_failed_items_in_request = False

        try:
            response = requests.get(manual_api_url, headers=headers)
            response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)

            response_data = response.json()
            # Correctly extract the list of items from the 'manualList' key
            items = response_data.get('manualList', [])

            if not items:
                print("  No items found in 'manualList' in the response for this manual ID.")
                print()
                continue

            check_count = 0
            for item in items:
                type_id = item.get("typeId")
                status = item.get("status")
                result = item.get("result")
                server_id = item.get("serverId") # Get serverId for historical check
                item_name = item.get("name", "N/A")

                # Condition for non-passing items: "typeId": is 3, "status": is not 1, "result": is not "Pass"
                # Using OR for status and result because if either indicates failure, it's a non-pass.
                if type_id == 3 and (status != 1 or result != "Pass"):
                    found_failed_items_in_request = True
                    check_count += 1
                    print(f"check {check_count}:")

                    item_manual_id = item.get("manualId")
                    detail_link = f"{base_detail_status_url}{item_manual_id}?isManual=true"

                    print(f"details status link: {detail_link}")
                    print(f'"manualId": {item_manual_id}')
                    print(f'"serverId": "{server_id}"')
                    print(f'"type": "{item.get("type", "N/A")}",')
                    print(f'"name": "{item_name}",')
                    print(f'"status": {status}')
                    print(f'"result": "{result}"')

                    # Add to the list of all failed checks for final summary
                    all_failed_detail_links_summary.append({"name": item_name, "link": detail_link})

                    # --- Historical Status Check ---
                    if server_id:
                        # Calculate UTC+0 hour for the startTime parameter
                        now_utc = datetime.datetime.now(datetime.timezone.utc)
                        # Format to YYYY-MM-DD HH:00:00
                        start_time_utc_str = now_utc.strftime("%Y-%m-%d %H:00:00")
                        # URL encode the time string
                        encoded_start_time = urllib.parse.quote(start_time_utc_str)

                        historical_status_url = (
                            f"{base_historical_status_api_url}?"
                            f"serverIds={server_id}&"
                            f"startTime={encoded_start_time}&"
                            f"timeDuration=1&" # Query for the last 1 hour
                            f"timeZone=0"    # UTC timezone
                        )

                        try:
                            hist_response = requests.get(historical_status_url, headers=headers)
                            hist_response.raise_for_status()
                            hist_data = hist_response.json()

                            is_consistently_failing = True # Assume true until a 'Pass' status is found
                            server_status_items = hist_data.get('serverStatusItems', [])

                            if not server_status_items:
                                # No data for this server in the last hour, so it's not consistently failing (i.e., "no")
                                is_consistently_failing = False
                            else:
                                display1_statuses = []
                                # The response structure shows display1 within the first item of serverStatusItems
                                if server_status_items and 'display1' in server_status_items[0]:
                                    display1_statuses = server_status_items[0].get('display1', [])

                                if not display1_statuses:
                                    # No status entries in display1, so not consistently failing (i.e., "no")
                                    is_consistently_failing = False
                                else:
                                    for d_item in display1_statuses:
                                        # If any status is 0 (Pass) or statusSuccess is true, then it's NOT consistently failing
                                        # Assuming serverStatus 0 means success
                                        if d_item.get("serverStatus") == 0 or d_item.get("statusSuccess") is True:
                                            is_consistently_failing = False
                                            break

                            if is_consistently_failing:
                                print("all status not pass in last 1h: yes")
                            else:
                                print("all status not pass in last 1h: no")
                                # Add to the list for new/intermittent failures
                                new_or_intermittent_failures_summary.append({"name": item_name, "link": detail_link})

                        except requests.exceptions.RequestException as hist_err:
                            print(f"  Error fetching historical status for serverId {server_id}: {hist_err}")
                            print("all status not pass in last 1h: N/A (Error fetching historical data)")
                        except json.JSONDecodeError:
                            print(f"  Failed to decode JSON from historical status for serverId {server_id}. Response: {hist_response.text}")
                            print("all status not pass in last 1h: N/A (Invalid historical data response)")
                    else:
                        print("  No serverId found for historical check.")
                    print() # Newline after historical check result for readability

            if not found_failed_items_in_request:
                print("all pass!")
            print() # Add a newline for better readability between requests

        except requests.exceptions.HTTPError as err:
            print(f"  HTTP error occurred for manualId {manual_id}: {err}")
            print(f"  Response content: {err.response.text}")
        except requests.exceptions.ConnectionError as err:
            print(f"  Connection error occurred for manualId {manual_id}: {err}")
        except requests.exceptions.Timeout as err:
            print(f"  Timeout error occurred for manualId {manual_id}: {err}")
        except requests.exceptions.RequestException as err:
            print(f"  An unexpected request error occurred for manualId {manual_id}: {err}")
        except json.JSONDecodeError:
            print(f"  Failed to decode JSON response for manualId {manual_id}. Response content: {response.text}")
        except Exception as e:
            print(f"  An unexpected error occurred for manualId {manual_id}: {e}")
        print() # Add a newline for better readability between requests

    # Print all collected details status links for all failed checks
    if all_failed_detail_links_summary:
        print("all fail check:")
        for item_summary in all_failed_detail_links_summary:
            print(f"check name: {item_summary['name']}")
            print(f"details status link: {item_summary['link']}")
        print() # Newline for separation

    # Print details for items where "all status not pass in last 1h: no"
    print("all fail check: (all status not pass in last 1h: no)")
    if new_or_intermittent_failures_summary:
        for item_summary in new_or_intermittent_failures_summary:
            print(f"check name: {item_summary['name']}")
            print(f"details status link: {item_summary['link']}")
    else:
        print("no case")

if __name__ == "__main__":
    main()

Case

Specify the manual test user as lewan and the test target zoneid list as 1221961, 2184411:

(venv_py39) lewan@LEWAN-M-6FV6 manual-test-script % python3.9 manual-test-zoneids.py -cec "lewan" -zoneids "1221961, 2184411"
request 1:
"zoneId": 1221961
ManualSessionId": "135752171"

request 2:
"zoneId": 2184411
ManualSessionId": "135752421"

ManualSessionIds: "135752171, 135752421"
(venv_py39) lewan@LEWAN-M-6FV6 manual-test-script % 

Use the manual test script output ManualSessionIds: "135752171, 135752421" as the input for the manual test result script to check the manual test result.

(venv_py39) lewan@LEWAN-M-6FV6 manual-test-script % python3.9 manual-test-result-manualids.py -manualids "135752171, 136062421"
request 1:
all pass!


request 2:
check 1:
details status link: https://mct.webex.com/detail-status/2112527601?isManual=true
"manualId": 2112527601
"serverId": "38319171"
"type": "URL Watch Server",
"name": "mttx2mul003.webex.com()",
"status": 2
"result": "Not Test"
all status not pass in last 1h: yes



all fail check:
check name: mttx2mul003.webex.com()
details status link: https://mct.webex.com/detail-status/2112527601?isManual=true

all fail check: (all status not pass in last 1h: no)
no case # which means mttx2mul003.webex.com check is always failed (all not pass in last hour), so ignore it here
(venv_py39) lewan@LEWAN-M-6FV6 manual-test-script % 

Search output

  • If you find "Init" or "Running", please wait for the test to finish.
  • If you find "No result", please check this check. For example, conduct a direct manual test on the page.

API case

These API are used in script, you can check them to better understand and modify the script.

Manual test with zoneid and cec

curl --location 'https://mctapi.prod.webex.com/mpi/test/manual/123' \
--header 'accept: application/json, text/plain, */*' \
--header 'accept-language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
--header 'authorization: {ci-token}' \
--header 'content-type: application/json; charset=UTF-8' \
--header 'username: lewan' \
--data '{
    "clusterId": "78791",
    "mtype": 2,
    "serverIp": "",
    "domainId": 0,
    "zoneId": 1221961,
    "serverId": 0,
    "serverType": 0,
    "protocol": 0,
    "agent": "wjfkgen-p-1",
    "identifier": "",
    "vGroupId": 1100100102
}'
response: 
{
    "errorCode": "OKOKOK",
    "localIp": "172.16.12.194",
    "status": "OKOKOK",
    "ManualSessionId": "135742671"
}

Check manual test result

curl --location 'https://mctapi.prod.webex.com/mpi/test/manual-id/135742671/0' \
--header 'accept: application/json, text/plain, */*' \
--header 'accept-language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
--header 'authorization: {ci-token}' \
--header 'content-type: application/json; charset=UTF-8' \
response: 
{
    "errorCode": "OKOKOK",
    "localIp": "172.16.12.194",
    "manualList": [
        {
            "id": 0,
            "manualId": 0,
            "serverId": null,
            "zoneType": 0,
            "name": "WebACD_Cluster_AS(plugin)",
            "type": "javaUrlStation",
            "typeId": 2,
            "result": "0/1",
            "tagID": "zone-WebACD_Cluster_AS(plugin)",
            "className": "child-of-null",
            "startTime": null,
            "endTime": null,
            "status": 1,
            "serverType": 0,
            "agent": null,
            "agentId": 0,
            "userName": null,
            "sourceId": 1100100102,
            "networkEnv": null
        },
        {
            "id": 0,
            "manualId": 2111681401,
            "serverId": "4875681",
            "zoneType": 7101,
            "name": "WebACD_Cluster_AS_Fail=Impact(WebACD_Cluster_AS_Fail=Impact)",
            "type": "jmeter",
            "typeId": 3,
            "result": "Pass",
            "tagID": "server-WebACD_Cluster_AS_Fail=Impact",
            "className": null,
            "startTime": null,
            "endTime": null,
            "status": 1,
            "serverType": 9401,
            "agent": null,
            "agentId": 0,
            "userName": null,
            "sourceId": 1100100102,
            "networkEnv": null
        }
    ],
    "totalCount": 0,
    "init": 0,
    "running": 0,
    "error": 0,
    "finish": 0,
    "testName": "Zone - WebACD_Cluster_AS(plugin)",
    "testStartTime": "2025-07-22 08:09:50",
    "testEndTime": "2025-07-22 08:09:58",
    "testStatus": "Pass",
    "progress": "100%",
    "failServerRate": "0/1",
    "result": "success",
    "clusterId": 78791,
    "agentId": 0,
    "sourceId": 1100100102
}

Query last hour statuses for a check

curl --location 'https://mctapi.prod.webex.com/mpi/clusterlist/queryZoneServerDetail?serverIds=38601496&startTime=2025-07-23%2009:00:00&timeDuration=1&timeZone=0' \
--header 'accept: application/json, text/plain, */*' \
--header 'accept-language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
--header 'authorization: {ci-token}' \
--header 'content-type: application/json; charset=UTF-8' \

response: 
{
    "errorCode": "OKOKOK",
    "localIp": "172.16.12.194",
    "serverStatusItems": [
        {
            "id": 38601496,
            "name": null,
            "type": 10001,
            "typeName": null,
            "status": 0,
            "childs": 0,
            "userId": 0,
            "hostname": "http://kubed-as.webex.com",
            "ip": "http://kubed-as.webex.com",
            "display1": [
                {
                    "zoneId": 2153201,
                    "serverId": 38601496,
                    "statusId": 5741175505,
                    "serverStatus": 0,
                    "timeshow": 1753261327000,
                    "displaytime": "2025-07-23 09:02:07",
                    "psttime": "2025-07-23 02:02:07",
                    "gmttime": "2025-07-23 09:02:07",
                    "hourpos": 0,
                    "protocol": 0,
                    "image": null,
                    "icon": 1,
                    "sourceId": 1100007903,
                    "agentName": "INTRL",
                    "domainId": 767991,
                    "serverType": 10001,
                    "pollingFlag": 1,
                    "resultType": 0,
                    "dbtime": "2025-07-23 09:02:07",
                    "lastFail": false,
                    "networkEnv": null,
                    "orgName": null,
                    "maintainStatus": false,
                    "forceMaintainStatus": false,
                    "statusFail": false,
                    "statusSuccess": true,
                    "statusIgnored": false,
                    "gastatus": true
                },
                {
                    "zoneId": 2153201,
                    "serverId": 38601496,
                    "statusId": 5741191830,
                    "serverStatus": 0,
                    "timeshow": 1753261601000,
                    "displaytime": "2025-07-23 09:06:41",
                    "psttime": "2025-07-23 02:06:41",
                    "gmttime": "2025-07-23 09:06:41",
                    "hourpos": 0,
                    "protocol": 0,
                    "image": null,
                    "icon": 1,
                    "sourceId": 1100007903,
                    "agentName": "INTRL",
                    "domainId": 767991,
                    "serverType": 10001,
                    "pollingFlag": 1,
                    "resultType": 0,
                    "dbtime": "2025-07-23 09:06:41",
                    "lastFail": false,
                    "networkEnv": null,
                    "orgName": null,
                    "maintainStatus": false,
                    "forceMaintainStatus": false,
                    "statusFail": false,
                    "statusSuccess": true,
                    "statusIgnored": false,
                    "gastatus": true
                }
            ],
            "display2": [],
            "display3": [],
            "display4": [],
            "lastPollError": false,
            "fourHourError": false,
            "lastHourError": false,
            "maintainReason": "",
            "flagPST": true,
            "zoneType": 7101,
            "zoneId": 2153201,
            "resultType": 0,
            "sourceId": 1100007903
        }
    ],
    "serverIds": "38601496",
    "failedZoneIds": []
}