diff --git a/README.md b/README.md index 2f27a5d..a2e7de7 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Complete the following steps to configure BrowserStack's XCUI step in Bitrise: | `Devices` | Provide one or more device-OS combination in a new line. For example:
`iPhone 11-13`
`iPhone XS-15` | Required | N/A | | `Instrumentation logs` | Generate instrumentation logs of the test session | Optional | `true` | | `Network logs` | Generate network logs of your test sessions to capture network traffic, latency, etc. | Optional | `false` | +| `Network logs options` | Refine network log capture as JSON (mirrors the `networkLogsOptions` capability). Supports `{"captureContent": true}` to capture request/response bodies, not just metadata. Automatically enables **Network logs**. | Optional | N/A | | `Device Logs` | Generate device logs | Optional | `false` | | `Capture screenshots` | Capture the screenshots of the test execution| Optional | `false` | | `Video recording` | Record video of the test execution | Optional | `true` | diff --git a/constants.go b/constants.go index c686f7d..0673d91 100644 --- a/constants.go +++ b/constants.go @@ -24,4 +24,6 @@ const ( RUNNER_APP_NOT_FOUND = "xcuitest_testsuite_path: couldn’t find the -Runner.app . Please add the $BITRISE_TEST_BUNDLE_PATH from Xcode Build for testing for iOS step or the absolute path of -Runner.app" IPA_NOT_FOUND = "app_ipa_path: couldn’t find the iOS app (.ipa file). Please add the $BITRISE_IPA_PATH from Xcode Archive & Export for iOS step or the absolute path of iOS app (.ipa file)" FILE_ZIP_ERROR = "Something went wrong while processing the test-suite, error: %s" + + NETWORK_LOGS_OPTIONS_INVALID = "Invalid network_logs_options. Expected JSON such as {\"captureContent\": true}. Error: %s" ) diff --git a/main_test.go b/main_test.go index ae730ce..777b2e4 100644 --- a/main_test.go +++ b/main_test.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "os" "testing" @@ -109,3 +110,71 @@ func TestLocateTestRunnerFileAndZip(t *testing.T) { os.Remove("test_suite.zip") } } + +func TestParseNetworkLogsOptions(t *testing.T) { + t.Log("It should treat an empty value as not set") + { + options, err := parseNetworkLogsOptions("") + require.NoError(t, err) + require.Nil(t, options) + } + t.Log("It should parse captureContent true") + { + options, err := parseNetworkLogsOptions(`{"captureContent": true}`) + require.NoError(t, err) + require.NotNil(t, options) + require.True(t, options.CaptureContent) + } + t.Log("It should parse captureContent false") + { + options, err := parseNetworkLogsOptions(`{"captureContent": false}`) + require.NoError(t, err) + require.NotNil(t, options) + require.False(t, options.CaptureContent) + } + t.Log("It should return an actionable error on malformed JSON") + { + _, err := parseNetworkLogsOptions(`{captureContent: true}`) + require.Error(t, err) + require.Contains(t, err.Error(), "network_logs_options") + } + t.Log("It should return an actionable error on a wrong value type") + { + _, err := parseNetworkLogsOptions(`{"captureContent": "true"}`) + require.Error(t, err) + require.Contains(t, err.Error(), "network_logs_options") + } +} + +func TestCreateBuildPayloadNetworkLogsOptions(t *testing.T) { + t.Setenv("devices_list", "iPhone 14-16") + + t.Log("captureContent true -> networkLogs forced true + networkLogsOptions.captureContent true") + { + t.Setenv("network_logs", "false") + t.Setenv("network_logs_options", `{"captureContent": true}`) + + payload := createBuildPayload() + require.True(t, payload.NetworkLogs) + require.NotNil(t, payload.NetworkLogsOptions) + require.True(t, payload.NetworkLogsOptions.CaptureContent) + + marshalled, err := json.Marshal(payload) + require.NoError(t, err) + assert.Contains(t, string(marshalled), `"networkLogs":true`) + assert.Contains(t, string(marshalled), `"networkLogsOptions":{"captureContent":true}`) + } + + t.Log("options unset -> networkLogsOptions omitted (backward compatible)") + { + t.Setenv("network_logs", "true") + t.Setenv("network_logs_options", "") + + payload := createBuildPayload() + require.Nil(t, payload.NetworkLogsOptions) + + marshalled, err := json.Marshal(payload) + require.NoError(t, err) + assert.NotContains(t, string(marshalled), "networkLogsOptions") + } +} diff --git a/step.yml b/step.yml index 7da4f1a..18bf559 100644 --- a/step.yml +++ b/step.yml @@ -130,6 +130,19 @@ inputs: - "true" - "false" category: 'Debug logs' + - network_logs_options: + opts: + title: 'Network logs options' + summary: 'Refine network log capture as JSON. Supports captureContent to capture request/response bodies.' + description: | + Provide network log options as JSON to refine what your network logs capture. This mirrors the `networkLogsOptions` capability used in local/IDE runs. + + Currently supported: + + `{"captureContent": true}` - capture the request and response content (bodies), in addition to metadata such as URLs, headers, latency and status codes. + + Enabling this automatically enables **Network Logs** (it has no effect on its own). + category: 'Debug logs' - device_logs: "false" opts: title: 'Device Logs' diff --git a/structs.go b/structs.go index 3d0741b..179bba8 100644 --- a/structs.go +++ b/structs.go @@ -12,22 +12,27 @@ type TestSharding struct { AutoStrategyDevices []string `json:"devices,omitempty"` } +type NetworkLogsOptions struct { + CaptureContent bool `json:"captureContent"` +} + type BrowserStackPayload struct { - App string `json:"app"` - TestSuite string `json:"testSuite"` - Devices []string `json:"devices"` - InstrumentationLogs bool `json:"instrumentationLogs"` - NetworkLogs bool `json:"networkLogs"` - DeviceLogs bool `json:"deviceLogs"` - DebugScreenshots bool `json:"debugscreenshots,omitempty"` - VideoRecording bool `json:"video"` - Project string `json:"project,omitempty"` - ProjectNotifyURL string `json:"projectNotifyURL,omitempty"` - UseLocal bool `json:"local,omitempty"` - SkipTesting []string `json:"skip-testing,omitempty"` - OnlyTesting []string `json:"only-testing,omitempty"` - DynamicTests bool `json:"dynamicTests,omitempty"` - UseTestSharding interface{} `json:"shards,omitempty"` + App string `json:"app"` + TestSuite string `json:"testSuite"` + Devices []string `json:"devices"` + InstrumentationLogs bool `json:"instrumentationLogs"` + NetworkLogs bool `json:"networkLogs"` + NetworkLogsOptions *NetworkLogsOptions `json:"networkLogsOptions,omitempty"` + DeviceLogs bool `json:"deviceLogs"` + DebugScreenshots bool `json:"debugscreenshots,omitempty"` + VideoRecording bool `json:"video"` + Project string `json:"project,omitempty"` + ProjectNotifyURL string `json:"projectNotifyURL,omitempty"` + UseLocal bool `json:"local,omitempty"` + SkipTesting []string `json:"skip-testing,omitempty"` + OnlyTesting []string `json:"only-testing,omitempty"` + DynamicTests bool `json:"dynamicTests,omitempty"` + UseTestSharding interface{} `json:"shards,omitempty"` // Apart from the inputs from UI, these are some more fields which we support. // We've mentioned the type and the json key for these field. diff --git a/util_fns.go b/util_fns.go index 0f1d7d9..189ffc6 100644 --- a/util_fns.go +++ b/util_fns.go @@ -95,6 +95,23 @@ func getTestFilters(payload *BrowserStackPayload) { } } +// parseNetworkLogsOptions parses the network_logs_options input, which mirrors +// the App Automate `networkLogsOptions` capability (e.g. {"captureContent": true}). +// An empty value means "not set". Invalid JSON is a hard, actionable error. +func parseNetworkLogsOptions(raw string) (*NetworkLogsOptions, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, nil + } + + options := NetworkLogsOptions{} + if err := json.Unmarshal([]byte(raw), &options); err != nil { + return nil, fmt.Errorf(NETWORK_LOGS_OPTIONS_INVALID, err) + } + + return &options, nil +} + // this util only picks data from env and map it to the struct func createBuildPayload() BrowserStackPayload { instrumentation_logs, _ := strconv.ParseBool(os.Getenv("instrumentation_logs")) @@ -105,6 +122,11 @@ func createBuildPayload() BrowserStackPayload { use_local, _ := strconv.ParseBool(os.Getenv("use_local")) use_dynamic_tests, _ := strconv.ParseBool(os.Getenv("use_dynamic_tests")) + network_logs_options, network_logs_options_err := parseNetworkLogsOptions(os.Getenv("network_logs_options")) + if network_logs_options_err != nil { + failf(network_logs_options_err.Error()) + } + sharding_data := TestSharding{} if os.Getenv("use_test_sharding") != "" { err := json.Unmarshal([]byte(os.Getenv("use_test_sharding")), &sharding_data) @@ -126,6 +148,17 @@ func createBuildPayload() BrowserStackPayload { UseLocal: use_local, } + // networkLogsOptions is only honoured by App Automate when networkLogs is + // also enabled, so enable it automatically when options are provided + // (avoids a backend "networkLogs not enabled" error). + if network_logs_options != nil { + if !network_logs { + log.Println("network_logs auto-enabled because network_logs_options was provided") + } + payload.NetworkLogs = true + payload.NetworkLogsOptions = network_logs_options + } + getTestFilters(&payload) if len(sharding_data.Mapping) != 0 && sharding_data.NumberOfShards != 0 {