Repository: codemagic-ci-cd/cli-tools (language: python)

Issue:
Feature: Validate application test information before submitting to TestFlight
**Related CLI action: [`app-store-connect publish`](https://github.com/codemagic-ci-cd/cli-tools/blob/master/docs/app-store-connect/publish.md) with [`--testflight`](https://github.com/codemagic-ci-cd/cli-tools/blob/master/docs/app-store-connect/publish.md#-t---testflight) option.**

A prerequisite to submit builds to TestFlight is to fill some inputs on test information page under “Beta App Information” and “Beta App Review Information” sections. In case some of the fields are not properly filled in, submitting the build to TestFlight for external testing, by [creating a `BetaAppReviewSubmission` resource](https://developer.apple.com/documentation/appstoreconnectapi/submit_an_app_for_beta_review), will fail. Unfortunately it fails with `500` error code, which does not indicate the failure reason one bit:
```
Creating new Beta App Review Submission: build: 813f968e-91fe-457a-965f-78064ad7ed35
POST https://api.appstoreconnect.apple.com/v1/betaAppReviewSubmissions returned 500: An unexpected error occurred. - An unexpected error occurred on the server side. If this issue continues, contact us at https://developer.apple.com/contact/.
Failed to publish capybara.ipa to App Store Connect.
```

This error has at least two consequences:
1. The error is misleading and can cause only further confusion for those who try to submit builds to TestFlight external testing.
2. Builds can be submitted to TestFlight external review only after processing their uploaded binary is completed by Apple. However this can take significant amount of time (upwards of 10 minutes on occasions) and it is far from desirable (especially in CI environment) to wait for the processing just to get an error on the next step.

The changes introduced in this pull request mitigate the abovementioned issued by performing application's test information validation before waiting for the build processing to be completed. In case the test information is lacking some required values, a relevant error message that points out missing values is thrown and the submission process is halted. Halting also means that there is no need to wait until the build processing is completed.

For example the app seen below is missing required "Feedback Email" value under "Beta App Information" section.

<img width="952" alt="Screenshot 2021-06-18 at 16 05 50" src="https://user-images.githubusercontent.com/2756611/122565629-60cd6780-d04f-11eb-9191-50e0585bc9dd.png">

Submission to TestFlight for the app with test information depicted above fails now as this:
```shell
> app-store-connect publish --path /Users/builder/clone/build/ios/ipa/banaan.ipa --key-id @env:APP_STORE_CONNECT_KEY_IDENTIFIER --issuer-id @env:APP_STORE_CONNECT_ISSUER_ID --private-key @env:APP_STORE_CONNECT_PRIVATE_KEY --testflight

Publish "/Users/builder/clone/build/ios/ipa/banaan.ipa" to App Store Connect
App name: Banaan
Bundle identifier: io.codemagic.banaan
Certificate expires: 2022-05-14T07:35:20.000+0000
Distribution type: App Store
Min os version: 10.1
Provisioned devices: N/A
Provisions all devices: No
Supported platforms: iPhoneOS
Version code: 1.0.75
Version: 1.0

Validate "/Users/builder/clone/build/ios/ipa/banaan.ipa" for App Store Connect
{
    "tool-version": "4.050.1210",
    "tool-path": "/Applications/Xcode-12.5.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/Frameworks/AppStoreService.framework",
    "success-message": "No errors validating archive at '/Users/builder/clone/build/ios/ipa/banaan.ipa'.",
    "os-version": "11.4.0"
}
No errors validating archive at '/Users/builder/clone/build/ios/ipa/banaan.ipa'.

Upload "/Users/builder/clone/build/ios/ipa/banaan.ipa" to App Store Connect
{
    "tool-version": "4.050.1210",
    "tool-path": "/Applications/Xcode-12.5.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/Frameworks/AppStoreService.framework",
    "success-message": "No errors uploading '/Users/builder/clone/build/ios/ipa/banaan.ipa'",
    "os-version": "11.4.0"
}
No errors uploading '/Users/builder/clone/build/ios/ipa/banaan.ipa'

Submit /Users/builder/clone/build/ios/ipa/banaan.ipa to TestFlight

Find application entry from App Store Connect for uploaded binary
Found 1 App matching specified filters: bundleId=io.codemagic.banaan.
-- App --
Id: 1481211155
Type: apps
Bundle id: io.codemagic.banaan
Name: Banaan Codemagic
Primary locale: en-GB
Sku: banaanike
Available in new territories: False
Is or ever was made for kids: False

Find freshly uploaded build
Did not find build matching uploaded version yet, it might be still processing. Waiting 30 seconds to try again
Did not find build matching uploaded version yet, it might be still processing. Waiting 30 seconds to try again
Did not find build matching uploaded version yet, it might be still processing. Waiting 30 seconds to try again
Did not find build matching uploaded version yet, it might be still processing. Waiting 30 seconds to try again
Did not find build matching uploaded version yet, it might be still processing. Waiting 30 seconds to try again

Published build is
-- Build --
Id: 52b94d84-4b53-4081-9464-0382b211bf9f
Type: builds
Expired: False
Icon asset token: 
	Template url: https://is1-ssl.mzstatic.com/image/thumb/Purple125/v4/d1/d0/de/d1d0de9d-77c3-d57e-6df3-79bf2c6a98a0/Icon-83.5@2x.png.png/{w}x{h}bb.{f}
	Height: 167
	Width: 167
Min os version: 10.1
Processing state: PROCESSING
Version: 1.0.75
Uses non exempt encryption: False
Uploaded date: 2021-06-18 06:05:04-07:00
Expiration date: 2021-09-16 06:05:04-07:00
-- Pre Release Version --
Id: c35a61b4-e6bb-4e93-bfed-b7655be3cca5
Type: preReleaseVersions
Platform: IOS
Version: 1.0

Complete test information is required to submit application Banaan Codemagic build for external testing.
App is missing required Beta App Information: Feedback Email.
Fill in test information at https://appstoreconnect.apple.com/apps/1481211155/testflight/test-info.
Failed to publish /Users/builder/clone/build/ios/ipa/banaan.ipa
``` 


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5c0d7c2..574bbe9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,24 @@
+Version 0.8.3
+-------------
+
+**Features**
+
+- Check if application has complete test information in App Store Connect before submitting a build for external testing with `app-store-connect publish --testflight`. This will enable the submission to fail fast with descriptive message instead of waiting until build processing completes by Apple and only then failing while creating the TestFlight submission.
+- Add `--max-build-processing-wait` option to configure maximum time that `app-store-connect publish` will wait for the package to be processed before failing the TestFlight submission.
+- Improve error message when waiting for package to be processed times out during TestFlight submission as part of `app-store-connect publish`.
+
+**Development / Docs**
+
+- Get build and related resources using App Store Connect API client directly for `app-store-connect publish` instead of reusing other `app-store-connect` actions to reduce unnecessary repetition in the terminal output.
+- Extract application lookup into a separate method for finding uploaded build.
+- Define new models for Apple API resources: `BetaAppLocalization` and `BetaAppReviewDetail`.
+- Implement new App Store Connect API client methods to consume endpoints to [list beta app Localizations of an app](https://developer.apple.com/documentation/appstoreconnectapi/list_all_beta_app_localizations_of_an_app) and to [read the beta app review details of an app](https://developer.apple.com/documentation/appstoreconnectapi/read_the_beta_app_review_details_resource_of_an_app).
+- Show default values for arguments of type `TypedCliArgument`.
+- Add documentation for action `app-store-connect builds get`.
+- Document `--max-build-processing-wait` option in `app-store-connect publish` action.
+- Show default values for arguments of type `TypedCliArgument`. 
+- Show the long version of CLI flag first in help messages and online documentation to reduce ambiguity. For example use `--testflight` instead of `-t` in help messages.
+
 Version 0.8.2
 -------------
 
diff --git a/doc.py b/doc.py
index f8c7d3e2..872a3f21 100755
--- a/doc.py
+++ b/doc.py
@@ -365,7 +365,10 @@ def _process_flag(arg: SerializedArgument) -> str:
         def _process_description(arg: SerializedArgument) -> str:
             description = arg.description.replace('*', r'\*')
             description += '. Multiple arguments' if arg.nargs else ''
-            return f'{description}. Default:&nbsp;`{arg.default}`' if arg.default else description
+            if arg.default and '[Default:' not in description:
+                return f'{description}. Default:&nbsp;`{arg.default}`'
+            else:
+                return description
 
         if not args:
             return
diff --git a/docs/app-store-connect/beta-build-localizations/create.md b/docs/app-store-connect/beta-build-localizations/create.md
index ebef6ff6..ab47746f 100644
--- a/docs/app-store-connect/beta-build-localizations/create.md
+++ b/docs/app-store-connect/beta-build-localizations/create.md
@@ -14,9 +14,9 @@ app-store-connect beta-build-localizations create [-h] [--log-stream STREAM] [--
     [--private-key PRIVATE_KEY]
     [--certificates-dir CERTIFICATES_DIRECTORY]
     [--profiles-dir PROFILES_DIRECTORY]
-    [-n WHATS_NEW]
+    [--whats-new WHATS_NEW]
     BUILD_ID_RESOURCE_ID
-    -l LOCALE
+    --locale LOCALE
 ```
 ### Required arguments for action `create`
 
@@ -24,13 +24,13 @@ app-store-connect beta-build-localizations create [-h] [--log-stream STREAM] [--
 
 
 Alphanumeric ID value of the Build
-##### `-l, --locale=da | de-DE | el | en-AU | en-CA | en-GB | en-US | es-ES | es-MX | fi | fr-CA | fr-FR | id | it | ja | ko | ms | nl-NL | no | pt-BR | pt-PT | ru | sv | th | tr | vi | zh-Hans | zh-Hant`
+##### `--locale, -l=da | de-DE | el | en-AU | en-CA | en-GB | en-US | es-ES | es-MX | fi | fr-CA | fr-FR | id | it | ja | ko | ms | nl-NL | no | pt-BR | pt-PT | ru | sv | th | tr | vi | zh-Hans | zh-Hant`
 
 
 The locale code name for displaying localized "What's new" content in TestFlight. Learn more from https://developer.apple.com/documentation/appstoreconnectapi/betabuildlocalizationcreaterequest/data/attributes
 ### Optional arguments for action `create`
 
-##### `-n, --whats-new=WHATS_NEW`
+##### `--whats-new, -n=WHATS_NEW`
 
 
 Describe the changes and additions to the build and indicate the features you would like your users to tests. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_WHATS_NEW`. Alternatively to entering` WHATS_NEW `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@file:` prefix followed by a path to the file containing the value. Example: `@env:<variable>` uses the value in the environment variable named `<variable>`, and `@file:<file_path>` uses the value from file at `<file_path>`.
diff --git a/docs/app-store-connect/beta-build-localizations/list.md b/docs/app-store-connect/beta-build-localizations/list.md
index ff37c054..bec8621d 100644
--- a/docs/app-store-connect/beta-build-localizations/list.md
+++ b/docs/app-store-connect/beta-build-localizations/list.md
@@ -14,7 +14,7 @@ app-store-connect beta-build-localizations list [-h] [--log-stream STREAM] [--no
     [--private-key PRIVATE_KEY]
     [--certificates-dir CERTIFICATES_DIRECTORY]
     [--profiles-dir PROFILES_DIRECTORY]
-    [-l LOCALE_OPTIONAL]
+    [--locale LOCALE_OPTIONAL]
     BUILD_ID_RESOURCE_ID
 ```
 ### Required arguments for action `list`
@@ -25,7 +25,7 @@ app-store-connect beta-build-localizations list [-h] [--log-stream STREAM] [--no
 Alphanumeric ID value of the Build
 ### Optional arguments for action `list`
 
-##### `-l, --locale=da | de-DE | el | en-AU | en-CA | en-GB | en-US | es-ES | es-MX | fi | fr-CA | fr-FR | id | it | ja | ko | ms | nl-NL | no | pt-BR | pt-PT | ru | sv | th | tr | vi | zh-Hans | zh-Hant`
+##### `--locale, -l=da | de-DE | el | en-AU | en-CA | en-GB | en-US | es-ES | es-MX | fi | fr-CA | fr-FR | id | it | ja | ko | ms | nl-NL | no | pt-BR | pt-PT | ru | sv | th | tr | vi | zh-Hans | zh-Hant`
 
 
 The locale code name for displaying localized "What's new" content in TestFlight. Learn more from https://developer.apple.com/documentation/appstoreconnectapi/betabuildlocalizationcreaterequest/data/attributes
diff --git a/docs/app-store-connect/beta-build-localizations/modify.md b/docs/app-store-connect/beta-build-localizations/modify.md
index 023ea715..877f04cc 100644
--- a/docs/app-store-connect/beta-build-localizations/modify.md
+++ b/docs/app-store-connect/beta-build-localizations/modify.md
@@ -14,7 +14,7 @@ app-store-connect beta-build-localizations modify [-h] [--log-stream STREAM] [--
     [--private-key PRIVATE_KEY]
     [--certificates-dir CERTIFICATES_DIRECTORY]
     [--profiles-dir PROFILES_DIRECTORY]
-    [-n WHATS_NEW]
+    [--whats-new WHATS_NEW]
     BETA_BUILD_LOCALIZATION_ID_RESOURCE_ID
 ```
 ### Required arguments for action `modify`
@@ -25,7 +25,7 @@ app-store-connect beta-build-localizations modify [-h] [--log-stream STREAM] [--
 Alphanumeric ID value of the Beta Build Localization
 ### Optional arguments for action `modify`
 
-##### `-n, --whats-new=WHATS_NEW`
+##### `--whats-new, -n=WHATS_NEW`
 
 
 Describe the changes and additions to the build and indicate the features you would like your users to tests. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_WHATS_NEW`. Alternatively to entering` WHATS_NEW `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@file:` prefix followed by a path to the file containing the value. Example: `@env:<variable>` uses the value in the environment variable named `<variable>`, and `@file:<file_path>` uses the value from file at `<file_path>`.
diff --git a/docs/app-store-connect/builds.md b/docs/app-store-connect/builds.md
index c60904f6..85155938 100644
--- a/docs/app-store-connect/builds.md
+++ b/docs/app-store-connect/builds.md
@@ -76,4 +76,5 @@ Enable verbose logging for commands
 
 |Action|Description|
 | :--- | :--- |
+|[`get`](builds/get.md)|Get information about a specific build|
 |[`pre-release-version`](builds/pre-release-version.md)|Get the prerelease version for a specific build|
diff --git a/docs/app-store-connect/builds/get.md b/docs/app-store-connect/builds/get.md
new file mode 100644
index 00000000..42426666
--- /dev/null
+++ b/docs/app-store-connect/builds/get.md
@@ -0,0 +1,80 @@
+
+get
+===
+
+
+**Get information about a specific build**
+### Usage
+```bash
+app-store-connect builds get [-h] [--log-stream STREAM] [--no-color] [--version] [-s] [-v]
+    [--log-api-calls]
+    [--json]
+    [--issuer-id ISSUER_ID]
+    [--key-id KEY_IDENTIFIER]
+    [--private-key PRIVATE_KEY]
+    [--certificates-dir CERTIFICATES_DIRECTORY]
+    [--profiles-dir PROFILES_DIRECTORY]
+    BUILD_ID_RESOURCE_ID
+```
+### Required arguments for action `get`
+
+##### `BUILD_ID_RESOURCE_ID`
+
+
+Alphanumeric ID value of the Build
+### Optional arguments for command `app-store-connect`
+
+##### `--log-api-calls`
+
+
+Turn on logging for App Store Connect API HTTP requests
+##### `--json`
+
+
+Whether to show the resource in JSON format
+##### `--issuer-id=ISSUER_ID`
+
+
+App Store Connect API Key Issuer ID. Identifies the issuer who created the authentication token. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_ISSUER_ID`. Alternatively to entering` ISSUER_ID `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@file:` prefix followed by a path to the file containing the value. Example: `@env:<variable>` uses the value in the environment variable named `<variable>`, and `@file:<file_path>` uses the value from file at `<file_path>`.
+##### `--key-id=KEY_IDENTIFIER`
+
+
+App Store Connect API Key ID. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_KEY_IDENTIFIER`. Alternatively to entering` KEY_IDENTIFIER `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@file:` prefix followed by a path to the file containing the value. Example: `@env:<variable>` uses the value in the environment variable named `<variable>`, and `@file:<file_path>` uses the value from file at `<file_path>`.
+##### `--private-key=PRIVATE_KEY`
+
+
+App Store Connect API private key used for JWT authentication to communicate with Apple services. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not provided, the key will be searched from the following directories in sequence for a private key file with the name `AuthKey_<key_identifier>.p8`: private_keys, ~/private_keys, ~/.private_keys, ~/.appstoreconnect/private_keys, where <key_identifier> is the value of --key-id. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_PRIVATE_KEY`. Alternatively to entering` PRIVATE_KEY `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@file:` prefix followed by a path to the file containing the value. Example: `@env:<variable>` uses the value in the environment variable named `<variable>`, and `@file:<file_path>` uses the value from file at `<file_path>`.
+##### `--certificates-dir=CERTIFICATES_DIRECTORY`
+
+
+Directory where the code signing certificates will be saved. Default:&nbsp;`$HOME/Library/MobileDevice/Certificates`
+##### `--profiles-dir=PROFILES_DIRECTORY`
+
+
+Directory where the provisioning profiles will be saved. Default:&nbsp;`$HOME/Library/MobileDevice/Provisioning Profiles`
+### Common options
+
+##### `-h, --help`
+
+
+show this help message and exit
+##### `--log-stream=stderr | stdout`
+
+
+Log output stream. Default `stderr`
+##### `--no-color`
+
+
+Do not use ANSI colors to format terminal output
+##### `--version`
+
+
+Show tool version and exit
+##### `-s, --silent`
+
+
+Disable log output for commands
+##### `-v, --verbose`
+
+
+Enable verbose logging for commands
\ No newline at end of file
diff --git a/docs/app-store-connect/publish.md b/docs/app-store-connect/publish.md
index 9c611959..7bdee3b6 100644
--- a/docs/app-store-connect/publish.md
+++ b/docs/app-store-connect/publish.md
@@ -15,12 +15,13 @@ app-store-connect publish [-h] [--log-stream STREAM] [--no-color] [--version] [-
     [--certificates-dir CERTIFICATES_DIRECTORY]
     [--profiles-dir PROFILES_DIRECTORY]
     [--path APPLICATION_PACKAGE_PATH_PATTERNS]
-    [-u APPLE_ID]
-    [-p APP_SPECIFIC_PASSWORD]
-    [-t]
-    [-l LOCALE_OPTIONAL_WITH_DEFAULT]
-    [-n WHATS_NEW]
+    [--apple-id APPLE_ID]
+    [--password APP_SPECIFIC_PASSWORD]
+    [--testflight]
+    [--locale LOCALE_OPTIONAL_WITH_DEFAULT]
+    [--whats-new WHATS_NEW]
     [--skip-package-validation]
+    [--max-build-processing-wait MAX_BUILD_PROCESSING_WAIT]
 ```
 ### Optional arguments for action `publish`
 
@@ -28,23 +29,23 @@ app-store-connect publish [-h] [--log-stream STREAM] [--no-color] [--version] [-
 
 
 Path to artifact (\*.ipa or \*.pkg). Can be either a path literal, or a glob pattern to match projects in working directory. Multiple arguments. Default:&nbsp;`**/*.ipa, **/*.pkg`
-##### `-u, --apple-id=APPLE_ID`
+##### `--apple-id, -u=APPLE_ID`
 
 
 App Store Connect username used for application package validation and upload if App Store Connect API key is not specified
-##### `-p, --password=APP_SPECIFIC_PASSWORD`
+##### `--password, -p=APP_SPECIFIC_PASSWORD`
 
 
 App-specific password used for application package validation and upload if App Store Connect API Key is not specified. Used together with --apple-id and should match pattern `abcd-abcd-abcd-abcd`. Create an app-specific password in the Security section of your Apple ID account. Learn more from https://support.apple.com/en-us/HT204397. If not given, the value will be checked from environment variable `APP_SPECIFIC_PASSWORD`. Alternatively to entering` APP_SPECIFIC_PASSWORD `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@file:` prefix followed by a path to the file containing the value. Example: `@env:<variable>` uses the value in the environment variable named `<variable>`, and `@file:<file_path>` uses the value from file at `<file_path>`.
-##### `-t, --testflight`
+##### `--testflight, -t`
 
 
 Submit an app for Testflight beta app review to allow external testing
-##### `-l, --locale=da | de-DE | el | en-AU | en-CA | en-GB | en-US | es-ES | es-MX | fi | fr-CA | fr-FR | id | it | ja | ko | ms | nl-NL | no | pt-BR | pt-PT | ru | sv | th | tr | vi | zh-Hans | zh-Hant`
+##### `--locale, -l=da | de-DE | el | en-AU | en-CA | en-GB | en-US | es-ES | es-MX | fi | fr-CA | fr-FR | id | it | ja | ko | ms | nl-NL | no | pt-BR | pt-PT | ru | sv | th | tr | vi | zh-Hans | zh-Hant`
 
 
 The locale code name for displaying localized "What's new" content in TestFlight. Learn more from https://developer.apple.com/documentation/appstoreconnectapi/betabuildlocalizationcreaterequest/data/attributes. Default:&nbsp;`en-US`
-##### `-n, --whats-new=WHATS_NEW`
+##### `--whats-new, -n=WHATS_NEW`
 
 
 Describe the changes and additions to the build and indicate the features you would like your users to tests. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_WHATS_NEW`. Alternatively to entering` WHATS_NEW `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@file:` prefix followed by a path to the file containing the value. Example: `@env:<variable>` uses the value in the environment variable named `<variable>`, and `@file:<file_path>` uses the value from file at `<file_path>`.
@@ -52,6 +53,10 @@ Describe the changes and additions to the build and indicate the features you wo
 
 
 Skip package validation before uploading it to App Store Connect. Use this switch to opt out from running `altool --validate-app` before uploading package to App Store connect. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_SKIP_PACKAGE_VALIDATION`.
+##### `--max-build-processing-wait=MAX_BUILD_PROCESSING_WAIT`
+
+
+Maximum amount of minutes to wait for the freshly uploaded build to be processed by Apple and retry submitting the build for beta review. If the processing is not finished within the specified timeframe, further submission will be terminated. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_MAX_BUILD_PROCESSING_WAIT`. [Default: 20]
 ### Optional arguments for command `app-store-connect`
 
 ##### `--log-api-calls`
diff --git a/src/codemagic/__version__.py b/src/codemagic/__version__.py
index d1fb1a08..de9825af 100644
--- a/src/codemagic/__version__.py
+++ b/src/codemagic/__version__.py
@@ -1,5 +1,5 @@
 __title__ = 'codemagic-cli-tools'
 __description__ = 'CLI tools used in Codemagic builds'
-__version__ = '0.8.2'
+__version__ = '0.8.3'
 __url__ = 'https://github.com/codemagic-ci-cd/cli-tools'
 __licence__ = 'GNU General Public License v3.0'
diff --git a/src/codemagic/apple/app_store_connect/apps/apps.py b/src/codemagic/apple/app_store_connect/apps/apps.py
index 0facbeba..ecdb1051 100644
--- a/src/codemagic/apple/app_store_connect/apps/apps.py
+++ b/src/codemagic/apple/app_store_connect/apps/apps.py
@@ -9,6 +9,8 @@
 from codemagic.apple.resources import App
 from codemagic.apple.resources import AppStoreState
 from codemagic.apple.resources import AppStoreVersion
+from codemagic.apple.resources import BetaAppLocalization
+from codemagic.apple.resources import BetaAppReviewDetail
 from codemagic.apple.resources import Build
 from codemagic.apple.resources import LinkedResourceData
 from codemagic.apple.resources import Platform
@@ -101,3 +103,25 @@ def list_app_store_versions(self,
         params = resource_filter.as_query_params() if resource_filter else None
         app_store_versions = self.client.paginate(url, params=params, page_size=None)
         return [AppStoreVersion(app_store_version) for app_store_version in app_store_versions]
+
+    def list_beta_app_localizations(self, app: Union[App, ResourceId]) -> List[BetaAppLocalization]:
+        """
+        https://developer.apple.com/documentation/appstoreconnectapi/list_all_beta_app_localizations_of_an_app
+        """
+        if isinstance(app, App):
+            url = app.relationships.betaAppLocalizations.links.related
+        else:
+            url = f'{self.client.API_URL}/apps/{app}/betaAppLocalizations'
+        response = self.client.session.get(url).json()
+        return [BetaAppLocalization(bal) for bal in response['data']]
+
+    def read_beta_app_review_detail(self, app: Union[App, ResourceId]) -> BetaAppReviewDetail:
+        """
+        https://developer.apple.com/documentation/appstoreconnectapi/read_the_beta_app_review_details_resource_of_an_app
+        """
+        if isinstance(app, App):
+            url = app.relationships.betaAppReviewDetail.links.related
+        else:
+            url = f'{self.client.API_URL}/apps/{app}/betaAppReviewDetail'
+        response = self.client.session.get(url).json()
+        return BetaAppReviewDetail(response['data'])
diff --git a/src/codemagic/apple/resources/__init__.py b/src/codemagic/apple/resources/__init__.py
index dfb01f37..215e6efc 100644
--- a/src/codemagic/apple/resources/__init__.py
+++ b/src/codemagic/apple/resources/__init__.py
@@ -1,6 +1,8 @@
 from .app import App
 from .app_store_version import AppStoreVersion
 from .app_store_version_submission import AppStoreVersionSubmission
+from .beta_app_localization import BetaAppLocalization
+from .beta_app_review_detail import BetaAppReviewDetail
 from .beta_app_review_submission import BetaAppReviewSubmission
 from .beta_build_localization import BetaBuildLocalization
 from .build import Build
diff --git a/src/codemagic/apple/resources/beta_app_localization.py b/src/codemagic/apple/resources/beta_app_localization.py
new file mode 100644
index 00000000..76a1ebbf
--- /dev/null
+++ b/src/codemagic/apple/resources/beta_app_localization.py
@@ -0,0 +1,27 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Optional
+
+from .enums import Locale
+from .resource import Relationship
+from .resource import Resource
+
+
+class BetaAppLocalization(Resource):
+    """
+    https://developer.apple.com/documentation/appstoreconnectapi/betaapplocalization
+    """
+
+    @dataclass
+    class Attributes(Resource.Attributes):
+        description: Optional[str]
+        feedbackEmail: Optional[str]
+        locale: Locale
+        marketingUrl: Optional[str]
+        privacyPolicyUrl: Optional[str]
+        tvOsPrivacyPolicy: Optional[str]
+
+    @dataclass
+    class Relationships(Resource.Relationships):
+        app: Relationship
diff --git a/src/codemagic/apple/resources/beta_app_review_detail.py b/src/codemagic/apple/resources/beta_app_review_detail.py
new file mode 100644
index 00000000..a7cbb65e
--- /dev/null
+++ b/src/codemagic/apple/resources/beta_app_review_detail.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Optional
+
+from .resource import Relationship
+from .resource import Resource
+
+
+class BetaAppReviewDetail(Resource):
+    """
+    https://developer.apple.com/documentation/appstoreconnectapi/betaapplocalization
+    """
+
+    @dataclass
+    class Attributes(Resource.Attributes):
+        contactEmail: Optional[str]
+        contactFirstName: Optional[str]
+        contactLastName: Optional[str]
+        contactPhone: Optional[str]
+        demoAccountName: Optional[str]
+        demoAccountPassword: Optional[str]
+        demoAccountRequired: bool
+        notes: Optional[str]
+
+    @dataclass
+    class Relationships(Resource.Relationships):
+        app: Relationship
diff --git a/src/codemagic/apple/resources/enums.py b/src/codemagic/apple/resources/enums.py
index 6ff2fafe..72f72dee 100644
--- a/src/codemagic/apple/resources/enums.py
+++ b/src/codemagic/apple/resources/enums.py
@@ -228,6 +228,8 @@ class ResourceType(_ResourceEnum):
     APPS = 'apps'
     APP_STORE_VERSIONS = 'appStoreVersions'
     APP_STORE_VERSION_SUBMISSIONS = 'appStoreVersionSubmissions'
+    BETA_APP_LOCALIZATIONS = 'betaAppLocalizations'
+    BETA_APP_REVIEW_DETAILS = 'betaAppReviewDetails'
     BETA_APP_REVIEW_SUBMISSIONS = 'betaAppReviewSubmissions'
     BETA_BUILD_LOCALIZATIONS = 'betaBuildLocalizations'
     BUILDS = 'builds'
diff --git a/src/codemagic/cli/argument/typed_cli_argument.py b/src/codemagic/cli/argument/typed_cli_argument.py
index 36b29369..e5012b2a 100644
--- a/src/codemagic/cli/argument/typed_cli_argument.py
+++ b/src/codemagic/cli/argument/typed_cli_argument.py
@@ -26,6 +26,7 @@
 class TypedCliArgument(Generic[T], metaclass=abc.ABCMeta):
     argument_type: Union[Type[T], Callable[[str], T]] = str  # type: ignore
     environment_variable_key: Optional[str] = None
+    default_value: Optional[T] = None
 
     def __init__(self, raw_value: str, from_environment=False):
         self._raw_value = raw_value
@@ -56,11 +57,22 @@ def _parse_value(self) -> T:
         return self._apply_type(value)
 
     @classmethod
-    def get_description(cls, properties: 'ArgumentProperties') -> str:
+    def get_description(cls, properties: 'ArgumentProperties', include_default=True) -> str:
         description = f'{properties.description.rstrip(".")}.'
         if cls.environment_variable_key is not None:
             description += '\nIf not given, the value will be checked from ' \
                            f'environment variable {Colors.CYAN(cls.environment_variable_key)}.'
+        if include_default:
+            try:
+                if cls.default_value:
+                    default_value = cls.default_value
+                else:
+                    default_value = (properties.argparse_kwargs or {})['default']
+            except KeyError:
+                pass
+            else:
+                default = ArgumentFormatter.format_default_value(default_value)
+                return '\n'.join([description, default])
         return description
 
     @classmethod
@@ -117,19 +129,23 @@ def _parse_value(self) -> T:
             return super()._parse_value()
 
     @classmethod
-    def get_description(cls, properties: 'ArgumentProperties') -> str:
-        description = super().get_description(properties)
+    def get_description(cls, properties: 'ArgumentProperties', include_default=True) -> str:
+        description = super().get_description(properties, include_default=False)
         usage = f'Alternatively to entering {Colors.CYAN(properties.key.upper())} in plaintext, ' \
                 f'it may also be specified using a "@env:" prefix followed by a environment variable name, ' \
                 f'or "@file:" prefix followed by a path to the file containing the value.'
         example = 'Example: "@env:<variable>" uses the value in the environment variable named "<variable>", ' \
                   'and "@file:<file_path>" uses the value from file at "<file_path>".'
-        try:
-            default_value = (properties.argparse_kwargs or {})['default']
-            default = ArgumentFormatter.format_default_value(default_value)
-            return '\n'.join([description, usage, example, default])
-        except KeyError:
-            return '\n'.join([description, usage, example])
+
+        if include_default:
+            try:
+                default_value = (properties.argparse_kwargs or {})['default']
+            except KeyError:
+                pass
+            else:
+                default = ArgumentFormatter.format_default_value(default_value)
+                return '\n'.join([description, usage, example, default])
+        return '\n'.join([description, usage, example])
 
     def __str__(self):
         return self._raw_value
diff --git a/src/codemagic/tools/_app_store_connect/abstract_base_action.py b/src/codemagic/tools/_app_store_connect/abstract_base_action.py
index 8d391679..d3cb51e5 100644
--- a/src/codemagic/tools/_app_store_connect/abstract_base_action.py
+++ b/src/codemagic/tools/_app_store_connect/abstract_base_action.py
@@ -13,10 +13,8 @@
 from codemagic.apple.resources import AppStoreState
 from codemagic.apple.resources import AppStoreVersionSubmission
 from codemagic.apple.resources import BetaBuildLocalization
-from codemagic.apple.resources import Build
 from codemagic.apple.resources import Locale
 from codemagic.apple.resources import Platform
-from codemagic.apple.resources import PreReleaseVersion
 from codemagic.apple.resources import ResourceId
 from codemagic.mixins import PathFinderMixin
 
@@ -55,15 +53,6 @@ def create_beta_build_localization(
             should_print: bool = True) -> BetaBuildLocalization:
         ...
 
-    def get_build(self, build_id: ResourceId, should_print: bool = True) -> Build:
-        ...
-
-    def get_build_pre_release_version(self, build_id: ResourceId, should_print: bool = True) -> PreReleaseVersion:
-        ...
-
-    def list_app_builds(self, application_id: ResourceId, should_print: bool = True) -> List[Build]:
-        ...
-
     def list_apps(self,
                   bundle_id_identifier: Optional[str] = None,
                   application_id: Optional[ResourceId] = None,
diff --git a/src/codemagic/tools/_app_store_connect/actions/publish_action.py b/src/codemagic/tools/_app_store_connect/actions/publish_action.py
index c627deff..b7bce72a 100644
--- a/src/codemagic/tools/_app_store_connect/actions/publish_action.py
+++ b/src/codemagic/tools/_app_store_connect/actions/publish_action.py
@@ -10,6 +10,8 @@
 from typing import Union
 
 from codemagic import cli
+from codemagic.apple import AppStoreConnectApiError
+from codemagic.apple.resources import App
 from codemagic.apple.resources import Build
 from codemagic.apple.resources import BuildProcessingState
 from codemagic.apple.resources import Locale
@@ -37,6 +39,7 @@ class PublishAction(AbstractBaseAction, metaclass=ABCMeta):
                 BuildArgument.LOCALE_OPTIONAL_WITH_DEFAULT,
                 BuildArgument.WHATS_NEW,
                 PublishArgument.SKIP_PACKAGE_VALIDATION,
+                PublishArgument.MAX_BUILD_PROCESSING_WAIT,
                 action_options={'requires_api_client': False})
     def publish(self,
                 application_package_path_patterns: Sequence[pathlib.Path],
@@ -45,11 +48,18 @@ def publish(self,
                 submit_to_testflight: Optional[bool] = None,
                 locale: Locale = BuildArgument.LOCALE_OPTIONAL_WITH_DEFAULT.get_default(),
                 whats_new: Optional[Types.WhatsNewArgument] = None,
-                skip_package_validation: Optional[bool] = None) -> None:
+                skip_package_validation: Optional[bool] = None,
+                max_build_processing_wait: Optional[Types.MaxBuildProcessingWait] = None) -> None:
         """
         Publish application packages to App Store and submit them to Testflight
         """
 
+        # Workaround to support overriding default value by environment variable.
+        if max_build_processing_wait:
+            max_processing_minutes = max_build_processing_wait.value
+        else:
+            max_processing_minutes = Types.MaxBuildProcessingWait.default_value
+
         if not (apple_id and app_specific_password):
             self._assert_api_client_credentials(
                 'Either Apple ID and app specific password or App Store Connect API key information is required.')
@@ -79,10 +89,10 @@ def publish(self,
                 self._publish_application_package(altool, application_package, validate_package)
                 if submit_to_testflight:
                     if isinstance(application_package, Ipa):
-                        self._submit_build_to_testflight(application_package, locale, whats_new)
+                        self._submit_build_to_testflight(application_package, locale, whats_new, max_processing_minutes)
                     else:
                         continue  # Cannot submit macOS packages to TestFlight, skip
-            except IOError as error:
+            except (IOError, ValueError) as error:
                 failed_packages.append(str(application_package.path))
                 self.logger.error(Colors.RED(error.args[0]))
 
@@ -102,14 +112,24 @@ def _publish_application_package(
             self.logger.info(Colors.YELLOW('\nSkip validating "%s" for App Store Connect'), application_package.path)
         self._upload_artifact_with_altool(altool, application_package.path)
 
-    def _submit_build_to_testflight(self, ipa: Ipa, locale: Locale, whats_new: Optional[Types.WhatsNewArgument]):
+    def _submit_build_to_testflight(
+        self,
+        ipa: Ipa,
+        locale: Locale,
+        whats_new: Optional[Types.WhatsNewArgument],
+        max_processing_minutes: int,
+    ):
         self.logger.info(Colors.BLUE('\nSubmit %s to TestFlight'), ipa.path)
+        app = self._get_uploaded_build_application(ipa)
+        build, pre_release_version = self._get_uploaded_build(app, ipa)
 
-        build, pre_release_version = self._get_uploaded_build(ipa)
-        self.create_beta_app_review_submission(build.id)
         if locale and whats_new:
             self.create_beta_build_localization(build.id, locale, whats_new)
 
+        self._assert_app_has_testflight_information(app)
+        build = self._wait_until_build_is_processed(build, max_processing_minutes)
+        self.create_beta_app_review_submission(build.id)
+
     def _find_build(
         self,
         app_id: ResourceId,
@@ -123,10 +143,15 @@ def _find_build(
         after the upload and as a result the API calls may not return it. Implement a
         simple retrying logic to overcome this issue.
         """
-        for build in self.list_app_builds(app_id, should_print=False):
+        try:
+            found_builds = self.api_client.apps.list_builds(app_id)
+        except AppStoreConnectApiError as api_error:
+            raise AppStoreConnectError(str(api_error))
+
+        for build in found_builds:
             if build.attributes.version == application_package.version_code:
-                pre_release_version = self.get_build_pre_release_version(build.id, should_print=False)
-                if pre_release_version.attributes.version == application_package.version:
+                pre_release_version = self.api_client.builds.read_pre_release_version(build.id)
+                if pre_release_version and pre_release_version.attributes.version == application_package.version:
                     return build, pre_release_version
 
         # Matching build was not found.
@@ -146,26 +171,37 @@ def _find_build(
             # There are no more retries left, give up.
             raise IOError(f'Did not find corresponding build from App Store versions for "{application_package.path}"')
 
-    def _wait_until_build_is_processed(self, build: Build, retries: int = 20, retry_wait_seconds: int = 30) -> Build:
-        if build.attributes.processingState is BuildProcessingState.PROCESSING:
-            self.logger.info(
-                'Build %s is still being processed, wait %d seconds and check again', build.id, retry_wait_seconds)
-            if retries > 0:
+    def _wait_until_build_is_processed(
+        self,
+        build: Build,
+        max_processing_minutes: int,
+        retry_wait_seconds: int = 30,
+    ) -> Build:
+        self.logger.info(Colors.BLUE('\nWait until processing build %s is completed'), build.id)
+
+        start_waiting = time.time()
+        while time.time() - start_waiting < max_processing_minutes * 60:
+            if build.attributes.processingState is BuildProcessingState.PROCESSING:
+                msg_template = 'Build %s is still being processed, wait %d seconds and check again'
+                self.logger.info(msg_template, build.id, retry_wait_seconds)
                 time.sleep(retry_wait_seconds)
-                return self._wait_until_build_is_processed(
-                    self.get_build(build.id, should_print=False),
-                    retries=retries-1,
-                    retry_wait_seconds=retry_wait_seconds,
-                )
+                try:
+                    build = self.api_client.builds.read(build)
+                except AppStoreConnectApiError as api_error:
+                    raise AppStoreConnectError(str(api_error))
+            elif build.attributes.processingState in (BuildProcessingState.FAILED, BuildProcessingState.INVALID):
+                raise IOError(f'Uploaded build {build.id} is {build.attributes.processingState.value.lower()}')
             else:
-                raise IOError(f'Waiting for build {build.id} processing timed out')
-        elif build.attributes.processingState in (BuildProcessingState.FAILED, BuildProcessingState.INVALID):
-            raise IOError(f'Uploaded build {build.id} is {build.attributes.processingState.value.lower()}')
-        else:
-            self.logger.info(Colors.BLUE('Processing build %s is completed'), build.id)
-            return build
+                self.logger.info(Colors.BLUE('Processing build %s is completed'), build.id)
+                return build
 
-    def _get_uploaded_build(self, application_package: Union[Ipa, MacOsPackage]) -> Tuple[Build, PreReleaseVersion]:
+        raise IOError((
+            f'Waiting for build {build.id} processing timed out in {max_processing_minutes} minutes. '
+            f'You can configure maximum timeout using {PublishArgument.MAX_BUILD_PROCESSING_WAIT.flag} '
+            f'command line option, or {Types.MaxBuildProcessingWait.environment_variable_key} environment variable.'
+        ))
+
+    def _get_uploaded_build_application(self, application_package: Union[Ipa, MacOsPackage]) -> App:
         bundle_id = application_package.bundle_identifier
         self.logger.info(Colors.BLUE('\nFind application entry from App Store Connect for uploaded binary'))
         try:
@@ -174,10 +210,12 @@ def _get_uploaded_build(self, application_package: Union[Ipa, MacOsPackage]) ->
             raise IOError(f'Did not find app with bundle identifier "{bundle_id}" from App Store Connect')
         else:
             self.printer.print_resource(app, True)
+        return app
 
+    def _get_uploaded_build(
+            self, app: App, application_package: Union[Ipa, MacOsPackage]) -> Tuple[Build, PreReleaseVersion]:
         self.logger.info(Colors.BLUE('\nFind freshly uploaded build'))
         build, pre_release_version = self._find_build(app.id, application_package)
-        build = self._wait_until_build_is_processed(build)
         self.logger.info(Colors.GREEN('\nPublished build is'))
         self.printer.print_resource(build, True)
         self.printer.print_resource(pre_release_version, True)
@@ -228,3 +266,43 @@ def _upload_artifact_with_altool(self, altool: Altool, artifact_path: pathlib.Pa
         result = altool.upload_app(artifact_path)
         message = result.success_message if result else f'No errors uploading "{artifact_path}".'
         self.logger.info(Colors.GREEN(message))
+
+    def _assert_app_has_testflight_information(self, app: App):
+        missing_beta_app_information = self._get_missing_beta_app_information(app)
+        missing_beta_app_review_information = self._get_missing_beta_app_review_information(app)
+
+        if not missing_beta_app_information and not missing_beta_app_review_information:
+            return  # All information required for TestFlight submission seems to be present
+
+        error_lines = []
+        if missing_beta_app_information:
+            missing_values = ', '.join(missing_beta_app_information)
+            error_lines.append(f'App is missing required Beta App Information: {missing_values}.')
+        if missing_beta_app_review_information:
+            missing_values = ', '.join(missing_beta_app_review_information)
+            error_lines.append(f'App is missing required Beta App Review Information: {missing_values}.')
+
+        name = app.attributes.name
+        raise ValueError('\n'.join([
+            f'Complete test information is required to submit application {name} build for external testing.',
+            *error_lines,
+            f'Fill in test information at https://appstoreconnect.apple.com/apps/{app.id}/testflight/test-info.',
+        ]))
+
+    def _get_missing_beta_app_information(self, app: App) -> List[str]:
+        beta_app_localizations = self.api_client.apps.list_beta_app_localizations(app)
+        default_beta_app_localization = beta_app_localizations[0]
+        required_test_information = {
+            'Feedback Email': default_beta_app_localization.attributes.feedbackEmail,
+        }
+        return [field_name for field_name, value in required_test_information.items() if not value]
+
+    def _get_missing_beta_app_review_information(self, app: App) -> List[str]:
+        beta_app_review_detail = self.api_client.apps.read_beta_app_review_detail(app)
+        required_test_information = {
+            'First Name': beta_app_review_detail.attributes.contactFirstName,
+            'Last Name': beta_app_review_detail.attributes.contactLastName,
+            'Phone Number': beta_app_review_detail.attributes.contactPhone,
+            'Email': beta_app_review_detail.attributes.contactEmail,
+        }
+        return [field_name for field_name, value in required_test_information.items() if not value]
diff --git a/src/codemagic/tools/_app_store_connect/arguments.py b/src/codemagic/tools/_app_store_connect/arguments.py
index b363ac3a..fab3b40d 100644
--- a/src/codemagic/tools/_app_store_connect/arguments.py
+++ b/src/codemagic/tools/_app_store_connect/arguments.py
@@ -62,6 +62,11 @@ class AppStoreConnectSkipPackageValidation(cli.TypedCliArgument[bool]):
         argument_type = bool
         environment_variable_key = 'APP_STORE_CONNECT_SKIP_PACKAGE_VALIDATION'
 
+    class MaxBuildProcessingWait(cli.TypedCliArgument[int]):
+        argument_type = int
+        environment_variable_key = 'APP_STORE_CONNECT_MAX_BUILD_PROCESSING_WAIT'
+        default_value = 20
+
 
 _API_DOCS_REFERENCE = f'Learn more at {AppStoreConnectApiClient.API_KEYS_DOCS_URL}.'
 
@@ -225,7 +230,7 @@ class PublishArgument(cli.Argument):
     )
     SUBMIT_TO_TESTFLIGHT = cli.ArgumentProperties(
         key='submit_to_testflight',
-        flags=('-t', '--testflight'),
+        flags=('--testflight', '-t'),
         type=bool,
         description='Submit an app for Testflight beta app review to allow external testing',
         argparse_kwargs={
@@ -235,7 +240,7 @@ class PublishArgument(cli.Argument):
     )
     APPLE_ID = cli.ArgumentProperties(
         key='apple_id',
-        flags=('-u', '--apple-id'),
+        flags=('--apple-id', '-u'),
         description=(
             'App Store Connect username used for application package validation '
             'and upload if App Store Connect API key is not specified'
@@ -244,7 +249,7 @@ class PublishArgument(cli.Argument):
     )
     APP_SPECIFIC_PASSWORD = cli.ArgumentProperties(
         key='app_specific_password',
-        flags=('-p', '--password'),
+        flags=('--password', '-p'),
         type=Types.AppSpecificPassword,
         description=(
             'App-specific password used for application package validation '
@@ -270,6 +275,19 @@ class PublishArgument(cli.Argument):
             'action': 'store_true',
         },
     )
+    MAX_BUILD_PROCESSING_WAIT = cli.ArgumentProperties(
+        key='max_build_processing_wait',
+        flags=('--max-build-processing-wait',),
+        type=Types.MaxBuildProcessingWait,
+        description=(
+            'Maximum amount of minutes to wait for the freshly uploaded build to be processed by '
+            'Apple and retry submitting the build for beta review. If the processing is not finished '
+            'within the specified timeframe, further submission will be terminated'
+        ),
+        argparse_kwargs={
+            'required': False,
+        },
+    )
 
 
 class BuildArgument(cli.Argument):
@@ -344,7 +362,7 @@ class BuildArgument(cli.Argument):
     )
     LOCALE = cli.ArgumentProperties(
         key='locale',
-        flags=('-l', '--locale'),
+        flags=('--locale', '-l'),
         type=Locale,
         description=(
             'The locale code name for displaying localized "What\'s new" content in TestFlight. '
@@ -367,7 +385,7 @@ class BuildArgument(cli.Argument):
     })
     WHATS_NEW = cli.ArgumentProperties(
         key='whats_new',
-        flags=('-n', '--whats-new'),
+        flags=('--whats-new', '-n'),
         type=Types.WhatsNewArgument,
         description=(
             'Describe the changes and additions to the build and indicate '


Test patch:
diff --git a/tests/apple/app_store_connect/apps/test_apps.py b/tests/apple/app_store_connect/apps/test_apps.py
index 16606442..b8b7a2ce 100644
--- a/tests/apple/app_store_connect/apps/test_apps.py
+++ b/tests/apple/app_store_connect/apps/test_apps.py
@@ -26,6 +26,23 @@ def test_read(self):
         assert app.id == app_id
         assert app.type is ResourceType.APPS
 
+    def test_read_beta_app_localizations(self):
+        banaan_app_id = ResourceId('1481211155')  # Banaan iOS
+        capybara_app_id = ResourceId('1496105355')  # CapybaraApp
+        for app_id in (banaan_app_id, capybara_app_id):
+            beta_app_localizations = self.api_client.apps.list_beta_app_localizations(app_id)
+            for beta_app_localization in beta_app_localizations:
+                assert beta_app_localization.type is ResourceType.BETA_APP_LOCALIZATIONS
+                assert beta_app_localization.relationships.app
+
+    def test_read_beta_app_review_detail(self):
+        banaan_app_id = ResourceId('1481211155')  # Banaan iOS
+        capybara_app_id = ResourceId('1496105355')  # CapybaraApp
+        for app_id in (banaan_app_id, capybara_app_id):
+            beta_app_review_detail = self.api_client.apps.read_beta_app_review_detail(app_id)
+            assert beta_app_review_detail.type is ResourceType.BETA_APP_REVIEW_DETAILS
+            assert beta_app_review_detail.relationships.app
+
     def test_read_not_found(self):
         with pytest.raises(AppStoreConnectApiError) as exception_info:
             self.api_client.apps.read(ResourceId('invalid-id'))
diff --git a/tests/tools/app_store_connect/test_publish_action.py b/tests/tools/app_store_connect/test_publish_action.py
index a09b7b70..fea05eff 100644
--- a/tests/tools/app_store_connect/test_publish_action.py
+++ b/tests/tools/app_store_connect/test_publish_action.py
@@ -87,7 +87,7 @@ def test_publish_action_with_localization_and_no_testflight_submission(publishin
             whats_new=Types.WhatsNewArgument("What's new"),
         )
 
-    assert str(error_info.value) == 'argument -n/--whats-new: --testflight is required for submitting notes'
+    assert str(error_info.value) == 'argument --whats-new/-n: --testflight is required for submitting notes'
 
 
 def test_publish_action_testflight_with_localization(publishing_namespace_kwargs):
@@ -96,6 +96,8 @@ def test_publish_action_testflight_with_localization(publishing_namespace_kwargs
             mock.patch.object(AppStoreConnect, '_get_publishing_application_packages') as mock_get_packages, \
             mock.patch.object(AppStoreConnect, '_upload_artifact_with_altool') as mock_upload, \
             mock.patch.object(AppStoreConnect, '_validate_artifact_with_altool') as mock_validate, \
+            mock.patch.object(AppStoreConnect, '_assert_app_has_testflight_information'), \
+            mock.patch.object(AppStoreConnect, '_get_uploaded_build_application') as mock_get_app, \
             mock.patch.object(AppStoreConnect, '_get_uploaded_build') as mock_get_build, \
             mock.patch.object(AppStoreConnect, 'create_beta_app_review_submission') as mock_create_review, \
             mock.patch.object(AppStoreConnect, 'create_beta_build_localization') as mock_create_localization:
@@ -103,6 +105,7 @@ def test_publish_action_testflight_with_localization(publishing_namespace_kwargs
         mock_find_paths.return_value = [ipa_path]
         mock_get_packages.return_value = [mock.create_autospec(Ipa, instance=True, path=ipa_path)]
         build = mock.Mock(id='1525e3c9-3015-407a-9ba5-9addd2558224')
+        mock_get_app.return_value = mock.Mock(id='1525e3c9-3015-407a-9ba5-9addd2558224')
         mock_get_build.return_value = [build, '1.0.0']
         locale = Locale('en-GB')