From a3688debf34937611eb7f6c36e8ccf52c24e89f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Mon, 11 Dec 2023 15:50:13 +0200 Subject: [PATCH] Add new and improve existing `app-store-connect` actions (#380) --- CHANGELOG.md | 36 +++++++ docs/app-store-connect/app-store-versions.md | 1 + .../app-store-versions/get.md | 95 ++++++++++++++++ .../app-store-versions/localizations.md | 7 ++ docs/app-store-connect/create-profile.md | 2 +- .../review-submission-items.md | 1 + .../review-submission-items/create.md | 6 +- .../review-submission-items/delete.md | 102 ++++++++++++++++++ docs/app-store-connect/review-submissions.md | 1 + .../review-submissions/items.md | 95 ++++++++++++++++ pyproject.toml | 2 +- src/codemagic/__version__.py | 2 +- .../app_store_connect/resource_manager.py | 9 +- .../versioning/review_submissions.py | 9 ++ .../apple/resources/error_response.py | 19 ++++ src/codemagic/models/simulator/simulator.py | 10 +- .../abstract_base_action.py | 1 + .../app_store_versions_action_group.py | 34 ++++++ .../review_submission_items_actions_group.py | 86 +++++++++++++-- .../review_submissions_actions_group.py | 20 ++++ .../tools/_app_store_connect/arguments.py | 18 ++++ .../tools/_app_store_connect/errors.py | 12 ++- .../resource_manager_mixin.py | 39 +++++-- src/codemagic/utilities/case_conversion.py | 9 ++ .../test_resource_manager.py | 17 --- tests/apple/resources/conftest.py | 6 ++ .../error_response_entity_state_invalid.json | 47 ++++++++ tests/apple/resources/test_error_response.py | 9 ++ tests/utilities/test_case_conversion.py | 37 +++++++ 29 files changed, 681 insertions(+), 51 deletions(-) create mode 100644 docs/app-store-connect/app-store-versions/get.md create mode 100644 docs/app-store-connect/review-submission-items/delete.md create mode 100644 docs/app-store-connect/review-submissions/items.md create mode 100644 src/codemagic/utilities/case_conversion.py create mode 100644 tests/apple/resources/mocks/error_response_entity_state_invalid.json create mode 100644 tests/utilities/test_case_conversion.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fd779ce1..bd5d5256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +Version 0.48.0 +------------- + +This PR contains changes from [PR #380](https://github.com/codemagic-ci-cd/cli-tools/pull/380) + +**Features** +- Add new actions: + - `app-store-connect app-store-versions get` to show App Store Version information. See official API method [documentation](https://developer.apple.com/documentation/appstoreconnectapi/read_app_store_version_information). + - `app-store-connect review-submission-items delete` to remove existing review submission item from App Store Connect. See official API method [documentation](https://developer.apple.com/documentation/appstoreconnectapi/delete_v1_reviewsubmissionitems_id). + - `app-store-connect review-submissions items` to list review submission items of specified review submission. See official API method [documentation](https://developer.apple.com/documentation/appstoreconnectapi/list_the_items_in_a_review_submission). +- Add option `--locale` to action `app-store-connect app-store-versions localizations` to filter retrieved localizations by given specified locales. +- Improve error message for action `app-store-connect review-submission-items create` if creating review submission item fails because required values are missing for application default locale on respective App Store Version. + +**Bugfixes** +- Fix invoking action `app-store-connect review-submission-items create` from command line. +- Do not require device IDs for action `app-store-connect create-profile` when not creating development or Ad Hoc provisioning profiles. + +**Development** +- Add new module `codemagic.utilities.case_conversion` with public functions `snake_to_camel` and `camel_to_snake`. +- Add new client method `list_items` to review submissions resource manager in `src/codemagic/apple/app_store_connect/versioning/review_submissions.py` to retrieve submission items list from App Store Connect. +- `AppStoreConnectError` exceptions now have field `api_error: Optional[ErrorResponse]` to store App Store Connect API error information. + +**Documentation** +- Update documentation for action groups + - `app-store-connect app-store-versions`, + - `app-store-connect review-submissions`, + - `app-store-connect review-submission-items`. +- Add documentation for actions: + - `app-store-connect app-store-versions get`, + - `app-store-connect review-submissions items`, + - `app-store-connect review-submission-items delete`. +- Update documentation for actions: + - `app-store-connect app-store-versions localizations`, + - `app-store-connect review-submission-items create`, + - `app-store-connect create-profile`. + Version 0.47.4 ------------- diff --git a/docs/app-store-connect/app-store-versions.md b/docs/app-store-connect/app-store-versions.md index d189c3dc..e53c2b9b 100644 --- a/docs/app-store-connect/app-store-versions.md +++ b/docs/app-store-connect/app-store-versions.md @@ -93,5 +93,6 @@ Enable verbose logging for commands | :--- | :--- | |[`create`](app-store-versions/create.md)|Add a new App Store version to an app using specified build.| |[`delete`](app-store-versions/delete.md)|Delete specified App Store version from Apple Developer portal| +|[`get`](app-store-versions/get.md)|Read App Store Version information| |[`localizations`](app-store-versions/localizations.md)|List All App Store Version Localizations for an App Store Version. Get a list of localized, version-level information about an app, for all locales.| |[`modify`](app-store-versions/modify.md)|Update the app store version for a specific app.| diff --git a/docs/app-store-connect/app-store-versions/get.md b/docs/app-store-connect/app-store-versions/get.md new file mode 100644 index 00000000..2a17fd3c --- /dev/null +++ b/docs/app-store-connect/app-store-versions/get.md @@ -0,0 +1,95 @@ + +get +=== + + +**Read App Store Version information** +### Usage +```bash +app-store-connect app-store-versions get [-h] [--log-stream STREAM] [--no-color] [--version] [-s] [-v] + [--log-api-calls] + [--api-unauthorized-retries UNAUTHORIZED_REQUEST_RETRIES] + [--api-server-error-retries SERVER_ERROR_RETRIES] + [--disable-jwt-cache] + [--json] + [--issuer-id ISSUER_ID] + [--key-id KEY_IDENTIFIER] + [--private-key PRIVATE_KEY] + [--certificates-dir CERTIFICATES_DIRECTORY] + [--profiles-dir PROFILES_DIRECTORY] + APP_STORE_VERSION_ID +``` +### Required arguments for action `get` + +##### `APP_STORE_VERSION_ID` + + +UUID value of the App Store Version +### Optional arguments for command `app-store-connect` + +##### `--log-api-calls` + + +Turn on logging for App Store Connect API HTTP requests +##### `--api-unauthorized-retries, -r=UNAUTHORIZED_REQUEST_RETRIES` + + +Specify how many times the App Store Connect API request should be retried in case the called request fails due to an authentication error (401 Unauthorized response from the server). In case of the above authentication error, the request is retried usinga new JSON Web Token as many times until the number of retries is exhausted. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_API_UNAUTHORIZED_RETRIES`. [Default: 3] +##### `--api-server-error-retries=SERVER_ERROR_RETRIES` + + +Specify how many times the App Store Connect API request should be retried in case the called request fails due to a server error (response with status code 5xx). In case of server error, the request is retried until the number of retries is exhausted. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_API_SERVER_ERROR_RETRIES`. [Default: 3] +##### `--disable-jwt-cache` + + +Turn off caching App Store Connect JSON Web Tokens to disk. By default generated tokens are cached to disk to be reused between separate processes, which can can reduce number of false positive authentication errors from App Store Connect API. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_DISABLE_JWT_CACHE`. +##### `--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 the environment variable `APP_STORE_CONNECT_ISSUER_ID`. Alternatively to entering `ISSUER_ID` in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +##### `--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 the environment variable `APP_STORE_CONNECT_KEY_IDENTIFIER`. Alternatively to entering `KEY_IDENTIFIER` in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +##### `--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_.p8`: private_keys, ~/private_keys, ~/.private_keys, ~/.appstoreconnect/private_keys, where is the value of `--key-id`. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_PRIVATE_KEY`. Alternatively to entering `PRIVATE_KEY` in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +##### `--certificates-dir=CERTIFICATES_DIRECTORY` + + +Directory where the code signing certificates will be saved. Default: `$HOME/Library/MobileDevice/Certificates` +##### `--profiles-dir=PROFILES_DIRECTORY` + + +Directory where the provisioning profiles will be saved. Default: `$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 diff --git a/docs/app-store-connect/app-store-versions/localizations.md b/docs/app-store-connect/app-store-versions/localizations.md index 5b7a75c1..111d2090 100644 --- a/docs/app-store-connect/app-store-versions/localizations.md +++ b/docs/app-store-connect/app-store-versions/localizations.md @@ -17,6 +17,7 @@ app-store-connect app-store-versions localizations [-h] [--log-stream STREAM] [- [--private-key PRIVATE_KEY] [--certificates-dir CERTIFICATES_DIRECTORY] [--profiles-dir PROFILES_DIRECTORY] + [--locale LOCALES] APP_STORE_VERSION_ID ``` ### Required arguments for action `localizations` @@ -25,6 +26,12 @@ app-store-connect app-store-versions localizations [-h] [--log-stream STREAM] [- UUID value of the App Store Version +### Optional arguments for action `localizations` + +##### `--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 App Store metadata in different languages. See available locale code names from https://developer.apple.com/documentation/appstoreconnectapi/betabuildlocalizationcreaterequest/data/attributes. Multiple arguments ### Optional arguments for command `app-store-connect` ##### `--log-api-calls` diff --git a/docs/app-store-connect/create-profile.md b/docs/app-store-connect/create-profile.md index 3adff506..14547433 100644 --- a/docs/app-store-connect/create-profile.md +++ b/docs/app-store-connect/create-profile.md @@ -39,7 +39,7 @@ Alphanumeric ID value of the Signing Certificate. Multiple arguments ##### `--device-ids=DEVICE_RESOURCE_IDS` -Alphanumeric ID value of the Device. Multiple arguments +Alphanumeric ID value of the Device. Required for development profile types. Multiple arguments ##### `--type=IOS_APP_ADHOC | IOS_APP_DEVELOPMENT | IOS_APP_INHOUSE | IOS_APP_STORE | MAC_APP_DEVELOPMENT | MAC_APP_DIRECT | MAC_APP_STORE | MAC_CATALYST_APP_DEVELOPMENT | MAC_CATALYST_APP_DIRECT | MAC_CATALYST_APP_STORE | TVOS_APP_ADHOC | TVOS_APP_DEVELOPMENT | TVOS_APP_INHOUSE | TVOS_APP_STORE` diff --git a/docs/app-store-connect/review-submission-items.md b/docs/app-store-connect/review-submission-items.md index c502684f..aa26f034 100644 --- a/docs/app-store-connect/review-submission-items.md +++ b/docs/app-store-connect/review-submission-items.md @@ -92,3 +92,4 @@ Enable verbose logging for commands |Action|Description| | :--- | :--- | |[`create`](review-submission-items/create.md)|Add contents to review submission for App Store review request| +|[`delete`](review-submission-items/delete.md)|Delete specified Review Submission item| diff --git a/docs/app-store-connect/review-submission-items/create.md b/docs/app-store-connect/review-submission-items/create.md index 010057b3..b2dbb2b9 100644 --- a/docs/app-store-connect/review-submission-items/create.md +++ b/docs/app-store-connect/review-submission-items/create.md @@ -17,7 +17,7 @@ app-store-connect review-submission-items create [-h] [--log-stream STREAM] [--n [--private-key PRIVATE_KEY] [--certificates-dir CERTIFICATES_DIRECTORY] [--profiles-dir PROFILES_DIRECTORY] - APP_STORE_VERSION_ID + REVIEW_SUBMISSION_ID --app-custom-product-page-version-id APP_CUSTOM_PRODUCT_PAGE_VERSION_ID --app-event-id APP_EVENT_ID --version-id APP_STORE_VERSION_ID @@ -25,10 +25,10 @@ app-store-connect review-submission-items create [-h] [--log-stream STREAM] [--n ``` ### Required arguments for action `create` -##### `APP_STORE_VERSION_ID` +##### `REVIEW_SUBMISSION_ID` -UUID value of the App Store Version +UUID value of the review submission ##### `--app-custom-product-page-version-id=APP_CUSTOM_PRODUCT_PAGE_VERSION_ID` diff --git a/docs/app-store-connect/review-submission-items/delete.md b/docs/app-store-connect/review-submission-items/delete.md new file mode 100644 index 00000000..1f77753c --- /dev/null +++ b/docs/app-store-connect/review-submission-items/delete.md @@ -0,0 +1,102 @@ + +delete +====== + + +**Delete specified Review Submission item** +### Usage +```bash +app-store-connect review-submission-items delete [-h] [--log-stream STREAM] [--no-color] [--version] [-s] [-v] + [--log-api-calls] + [--api-unauthorized-retries UNAUTHORIZED_REQUEST_RETRIES] + [--api-server-error-retries SERVER_ERROR_RETRIES] + [--disable-jwt-cache] + [--json] + [--issuer-id ISSUER_ID] + [--key-id KEY_IDENTIFIER] + [--private-key PRIVATE_KEY] + [--certificates-dir CERTIFICATES_DIRECTORY] + [--profiles-dir PROFILES_DIRECTORY] + [--ignore-not-found] + REVIEW_SUBMISSION_ITEM_ID +``` +### Required arguments for action `delete` + +##### `REVIEW_SUBMISSION_ITEM_ID` + + +UUID value of the review submission +### Optional arguments for action `delete` + +##### `--ignore-not-found` + + +Do not raise exceptions if the specified resource does not exist. +### Optional arguments for command `app-store-connect` + +##### `--log-api-calls` + + +Turn on logging for App Store Connect API HTTP requests +##### `--api-unauthorized-retries, -r=UNAUTHORIZED_REQUEST_RETRIES` + + +Specify how many times the App Store Connect API request should be retried in case the called request fails due to an authentication error (401 Unauthorized response from the server). In case of the above authentication error, the request is retried usinga new JSON Web Token as many times until the number of retries is exhausted. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_API_UNAUTHORIZED_RETRIES`. [Default: 3] +##### `--api-server-error-retries=SERVER_ERROR_RETRIES` + + +Specify how many times the App Store Connect API request should be retried in case the called request fails due to a server error (response with status code 5xx). In case of server error, the request is retried until the number of retries is exhausted. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_API_SERVER_ERROR_RETRIES`. [Default: 3] +##### `--disable-jwt-cache` + + +Turn off caching App Store Connect JSON Web Tokens to disk. By default generated tokens are cached to disk to be reused between separate processes, which can can reduce number of false positive authentication errors from App Store Connect API. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_DISABLE_JWT_CACHE`. +##### `--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 the environment variable `APP_STORE_CONNECT_ISSUER_ID`. Alternatively to entering `ISSUER_ID` in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +##### `--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 the environment variable `APP_STORE_CONNECT_KEY_IDENTIFIER`. Alternatively to entering `KEY_IDENTIFIER` in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +##### `--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_.p8`: private_keys, ~/private_keys, ~/.private_keys, ~/.appstoreconnect/private_keys, where is the value of `--key-id`. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_PRIVATE_KEY`. Alternatively to entering `PRIVATE_KEY` in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +##### `--certificates-dir=CERTIFICATES_DIRECTORY` + + +Directory where the code signing certificates will be saved. Default: `$HOME/Library/MobileDevice/Certificates` +##### `--profiles-dir=PROFILES_DIRECTORY` + + +Directory where the provisioning profiles will be saved. Default: `$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 diff --git a/docs/app-store-connect/review-submissions.md b/docs/app-store-connect/review-submissions.md index b6ec61ca..b60ebefe 100644 --- a/docs/app-store-connect/review-submissions.md +++ b/docs/app-store-connect/review-submissions.md @@ -95,3 +95,4 @@ Enable verbose logging for commands |[`confirm`](review-submissions/confirm.md)|Confirm pending review submission for App Review| |[`create`](review-submissions/create.md)|Create a review submission request for application's latest App Store Version| |[`get`](review-submissions/get.md)|Read Review Submission information| +|[`items`](review-submissions/items.md)|List review submission items for specified review submission| diff --git a/docs/app-store-connect/review-submissions/items.md b/docs/app-store-connect/review-submissions/items.md new file mode 100644 index 00000000..fa4d3b8d --- /dev/null +++ b/docs/app-store-connect/review-submissions/items.md @@ -0,0 +1,95 @@ + +items +===== + + +**List review submission items for specified review submission** +### Usage +```bash +app-store-connect review-submissions items [-h] [--log-stream STREAM] [--no-color] [--version] [-s] [-v] + [--log-api-calls] + [--api-unauthorized-retries UNAUTHORIZED_REQUEST_RETRIES] + [--api-server-error-retries SERVER_ERROR_RETRIES] + [--disable-jwt-cache] + [--json] + [--issuer-id ISSUER_ID] + [--key-id KEY_IDENTIFIER] + [--private-key PRIVATE_KEY] + [--certificates-dir CERTIFICATES_DIRECTORY] + [--profiles-dir PROFILES_DIRECTORY] + REVIEW_SUBMISSION_ID +``` +### Required arguments for action `items` + +##### `REVIEW_SUBMISSION_ID` + + +UUID value of the review submission +### Optional arguments for command `app-store-connect` + +##### `--log-api-calls` + + +Turn on logging for App Store Connect API HTTP requests +##### `--api-unauthorized-retries, -r=UNAUTHORIZED_REQUEST_RETRIES` + + +Specify how many times the App Store Connect API request should be retried in case the called request fails due to an authentication error (401 Unauthorized response from the server). In case of the above authentication error, the request is retried usinga new JSON Web Token as many times until the number of retries is exhausted. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_API_UNAUTHORIZED_RETRIES`. [Default: 3] +##### `--api-server-error-retries=SERVER_ERROR_RETRIES` + + +Specify how many times the App Store Connect API request should be retried in case the called request fails due to a server error (response with status code 5xx). In case of server error, the request is retried until the number of retries is exhausted. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_API_SERVER_ERROR_RETRIES`. [Default: 3] +##### `--disable-jwt-cache` + + +Turn off caching App Store Connect JSON Web Tokens to disk. By default generated tokens are cached to disk to be reused between separate processes, which can can reduce number of false positive authentication errors from App Store Connect API. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_DISABLE_JWT_CACHE`. +##### `--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 the environment variable `APP_STORE_CONNECT_ISSUER_ID`. Alternatively to entering `ISSUER_ID` in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +##### `--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 the environment variable `APP_STORE_CONNECT_KEY_IDENTIFIER`. Alternatively to entering `KEY_IDENTIFIER` in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +##### `--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_.p8`: private_keys, ~/private_keys, ~/.private_keys, ~/.appstoreconnect/private_keys, where is the value of `--key-id`. If not given, the value will be checked from the environment variable `APP_STORE_CONNECT_PRIVATE_KEY`. Alternatively to entering `PRIVATE_KEY` in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +##### `--certificates-dir=CERTIFICATES_DIRECTORY` + + +Directory where the code signing certificates will be saved. Default: `$HOME/Library/MobileDevice/Certificates` +##### `--profiles-dir=PROFILES_DIRECTORY` + + +Directory where the provisioning profiles will be saved. Default: `$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 diff --git a/pyproject.toml b/pyproject.toml index 5c6d5602..b4747d32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "codemagic-cli-tools" -version = "0.47.4" +version = "0.48.0" description = "CLI tools used in Codemagic builds" readme = "README.md" authors = [ diff --git a/src/codemagic/__version__.py b/src/codemagic/__version__.py index 6b001c93..e747db3b 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.47.4.dev" +__version__ = "0.48.0.dev" __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/resource_manager.py b/src/codemagic/apple/app_store_connect/resource_manager.py index 124aee04..bb40cc1b 100644 --- a/src/codemagic/apple/app_store_connect/resource_manager.py +++ b/src/codemagic/apple/app_store_connect/resource_manager.py @@ -2,7 +2,6 @@ import abc import enum -import re import shlex from typing import TYPE_CHECKING from typing import Any @@ -18,6 +17,7 @@ from codemagic.apple.resources import Resource from codemagic.apple.resources import ResourceId from codemagic.apple.resources import ResourceType +from codemagic.utilities import case_conversion if TYPE_CHECKING: from codemagic.apple import AppStoreConnectApiClient @@ -30,12 +30,7 @@ class ResourceManager(Generic[R], metaclass=abc.ABCMeta): class Filter: @classmethod def _get_field_name(cls, field_name) -> str: - return cls._snake_to_camel(field_name) - - @classmethod - def _snake_to_camel(cls, field_name: str) -> str: - patt = re.compile(r"_(\w)") - return patt.sub(lambda m: m.group(1).upper(), field_name) + return case_conversion.snake_to_camel(field_name) @classmethod def _get_param_value(cls, field_value) -> str: diff --git a/src/codemagic/apple/app_store_connect/versioning/review_submissions.py b/src/codemagic/apple/app_store_connect/versioning/review_submissions.py index 49233b68..b43aae00 100644 --- a/src/codemagic/apple/app_store_connect/versioning/review_submissions.py +++ b/src/codemagic/apple/app_store_connect/versioning/review_submissions.py @@ -12,6 +12,7 @@ from codemagic.apple.resources import ResourceId from codemagic.apple.resources import ResourceType from codemagic.apple.resources import ReviewSubmission +from codemagic.apple.resources import ReviewSubmissionItem from codemagic.apple.resources import ReviewSubmissionState @@ -99,3 +100,11 @@ def modify( json=payload, ).json() return ReviewSubmission(response["data"]) + + def list_items(self, review_submission: Union[LinkedResourceData, ResourceId]) -> List[ReviewSubmissionItem]: + """ + https://developer.apple.com/documentation/appstoreconnectapi/list_the_items_in_a_review_submission + """ + review_submission_id = self._get_resource_id(review_submission) + url = f"{self.client.API_URL}/reviewSubmissions/{review_submission_id}/items" + return [ReviewSubmissionItem(item) for item in self.client.paginate(url)] diff --git a/src/codemagic/apple/resources/error_response.py b/src/codemagic/apple/resources/error_response.py index 6dd3e1a9..d265877b 100644 --- a/src/codemagic/apple/resources/error_response.py +++ b/src/codemagic/apple/resources/error_response.py @@ -3,6 +3,7 @@ import textwrap from dataclasses import dataclass from typing import Dict +from typing import Iterable from typing import List from typing import Optional @@ -43,6 +44,19 @@ class Error(DictSerializable): meta: Optional[ErrorMeta] = None links: Optional[Dict[str, str]] = None + @property + def associated_errors(self) -> Dict[str, List[Error]]: + if not self.meta or not self.meta.associatedErrors: + return {} + return self.meta.associatedErrors + + @property + def source_pointer(self) -> Optional[str]: + try: + return self.source["pointer"] if self.source else None + except KeyError: + return None + def __post_init__(self): if isinstance(self.meta, dict): self.meta = ErrorMeta(**self.meta) @@ -78,5 +92,10 @@ def from_raw_response(cls, response: Response) -> ErrorResponse: ) return error_response + def iter_associated_errors(self) -> Iterable[Error]: + for error in self.errors: + for errors in error.associated_errors.values(): + yield from errors + def __str__(self): return "\n".join(map(str, self.errors)) diff --git a/src/codemagic/models/simulator/simulator.py b/src/codemagic/models/simulator/simulator.py index 8713b4a2..4d80b157 100644 --- a/src/codemagic/models/simulator/simulator.py +++ b/src/codemagic/models/simulator/simulator.py @@ -5,7 +5,6 @@ import re import subprocess from dataclasses import dataclass -from functools import lru_cache from typing import Dict from typing import List from typing import Optional @@ -13,6 +12,7 @@ from typing import Union from codemagic.mixins import RunningCliAppMixin +from codemagic.utilities import case_conversion from .runtime import Runtime @@ -46,15 +46,11 @@ def dict(self) -> Dict[str, Union[str, bool]]: @classmethod def create(cls, **kwargs) -> Simulator: - @lru_cache() - def camel_to_snake(s: str) -> str: - return re.sub(r"([A-Z])", lambda m: f"_{m.group(1).lower()}", s) - return Simulator( **{ - camel_to_snake(name): value + case_conversion.camel_to_snake(name): value for name, value in kwargs.items() - if camel_to_snake(name) in cls.__dataclass_fields__ # type: ignore + if case_conversion.camel_to_snake(name) in cls.__dataclass_fields__ # type: ignore }, ) 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 dbc52586..58b2e792 100644 --- a/src/codemagic/tools/_app_store_connect/abstract_base_action.py +++ b/src/codemagic/tools/_app_store_connect/abstract_base_action.py @@ -264,6 +264,7 @@ def cancel_review_submissions( def list_app_store_version_localizations( self, app_store_version_id: ResourceId, + locales: Optional[Sequence[Locale]] = None, should_print: bool = True, ) -> List[AppStoreVersionLocalization]: from .action_groups import AppStoreVersionsActionGroup diff --git a/src/codemagic/tools/_app_store_connect/action_groups/app_store_versions_action_group.py b/src/codemagic/tools/_app_store_connect/action_groups/app_store_versions_action_group.py index a693c863..24b01ad0 100644 --- a/src/codemagic/tools/_app_store_connect/action_groups/app_store_versions_action_group.py +++ b/src/codemagic/tools/_app_store_connect/action_groups/app_store_versions_action_group.py @@ -4,6 +4,7 @@ from datetime import datetime from typing import List from typing import Optional +from typing import Sequence from typing import Union from codemagic import cli @@ -12,6 +13,7 @@ from codemagic.apple.resources import AppStoreVersion from codemagic.apple.resources import AppStoreVersionLocalization from codemagic.apple.resources import Build +from codemagic.apple.resources import Locale from codemagic.apple.resources import Platform from codemagic.apple.resources import ReleaseType from codemagic.apple.resources import ResourceId @@ -19,6 +21,7 @@ from ..abstract_base_action import AbstractBaseAction from ..action_group import AppStoreConnectActionGroup from ..arguments import AppStoreVersionArgument +from ..arguments import AppStoreVersionLocalizationArgument from ..arguments import BuildArgument from ..arguments import CommonArgument from ..arguments import Types @@ -72,6 +75,28 @@ def create_app_store_version( **{k: v for k, v in create_params.items() if v is not None}, ) + @cli.action( + "get", + AppStoreVersionArgument.APP_STORE_VERSION_ID, + action_group=AppStoreConnectActionGroup.APP_STORE_VERSIONS, + ) + def get_app_store_version( + self, + app_store_version_id: Union[ResourceId, AppStoreVersion], + should_print: bool = True, + ) -> AppStoreVersion: + """ + Read App Store Version information + """ + if isinstance(app_store_version_id, AppStoreVersion): + app_store_version_id = app_store_version_id.id + + return self._get_resource( + app_store_version_id, + self.api_client.app_store_versions, + should_print, + ) + @cli.action( "modify", AppStoreVersionArgument.APP_STORE_VERSION_ID, @@ -148,17 +173,25 @@ def _get_build_version(self, version_string: Optional[str], build: Build) -> str @cli.action( "localizations", AppStoreVersionArgument.APP_STORE_VERSION_ID, + AppStoreVersionLocalizationArgument.LOCALES, action_group=AppStoreConnectActionGroup.APP_STORE_VERSIONS, ) def list_app_store_version_localizations( self, app_store_version_id: ResourceId, + locales: Optional[Sequence[Locale]] = None, should_print: bool = True, ) -> List[AppStoreVersionLocalization]: """ List All App Store Version Localizations for an App Store Version. Get a list of localized, version-level information about an app, for all locales. """ + + def predicate(app_store_version_localization: AppStoreVersionLocalization) -> bool: + if not locales: + return True + return app_store_version_localization.attributes.locale in locales + return self._list_related_resources( app_store_version_id, AppStoreVersion, @@ -166,4 +199,5 @@ def list_app_store_version_localizations( self.api_client.app_store_versions.list_app_store_version_localizations, None, should_print, + filter_predicate=predicate, ) diff --git a/src/codemagic/tools/_app_store_connect/action_groups/review_submission_items_actions_group.py b/src/codemagic/tools/_app_store_connect/action_groups/review_submission_items_actions_group.py index fa4f2d4e..68f75783 100644 --- a/src/codemagic/tools/_app_store_connect/action_groups/review_submission_items_actions_group.py +++ b/src/codemagic/tools/_app_store_connect/action_groups/review_submission_items_actions_group.py @@ -1,22 +1,30 @@ from __future__ import annotations from abc import ABCMeta +from typing import List from typing import Optional +from typing import Union from codemagic import cli +from codemagic.apple.resources import AppStoreVersionLocalization +from codemagic.apple.resources import ErrorResponse from codemagic.apple.resources import ResourceId from codemagic.apple.resources import ReviewSubmissionItem +from codemagic.cli import Colors +from codemagic.utilities import case_conversion from ..abstract_base_action import AbstractBaseAction from ..action_group import AppStoreConnectActionGroup -from ..arguments import AppStoreVersionArgument +from ..arguments import CommonArgument from ..arguments import ReviewSubmissionArgument +from ..arguments import ReviewSubmissionItemArgument +from ..errors import AppStoreConnectError class ReviewSubmissionItemsActionGroup(AbstractBaseAction, metaclass=ABCMeta): @cli.action( "create", - AppStoreVersionArgument.APP_STORE_VERSION_ID, + ReviewSubmissionArgument.REVIEW_SUBMISSION_ID, ReviewSubmissionArgument.APP_CUSTOM_PRODUCT_PAGE_VERSION_ID, ReviewSubmissionArgument.APP_EVENT_ID, ReviewSubmissionArgument.APP_STORE_VERSION_ID, @@ -41,9 +49,75 @@ def create_review_submission_item( "app_store_version": app_store_version_id, "app_store_version_experiment": app_store_version_experiment_id, } - return self._create_resource( + try: + return self._create_resource( + self.api_client.review_submissions_items, + should_print, + review_submission=review_submission_id, + **{k: v for k, v in optional_kwargs.items() if v is not None}, + ) + except AppStoreConnectError as asc_error: + if asc_error.api_error: + self._on_create_review_submission_item_error(asc_error.api_error) + raise + + @cli.action( + "delete", + ReviewSubmissionItemArgument.REVIEW_SUBMISSION_ITEM_ID, + CommonArgument.IGNORE_NOT_FOUND, + action_group=AppStoreConnectActionGroup.REVIEW_SUBMISSION_ITEMS, + ) + def delete_review_submission_item( + self, + review_submission_item_id: Union[ResourceId, ReviewSubmissionItem], + ignore_not_found: bool = False, + ) -> None: + """ + Delete specified Review Submission item + """ + if isinstance(review_submission_item_id, ReviewSubmissionItem): + review_submission_item_id = review_submission_item_id.id + + self._delete_resource( self.api_client.review_submissions_items, - should_print, - review_submission=review_submission_id, - **{k: v for k, v in optional_kwargs.items() if v is not None}, + review_submission_item_id, + ignore_not_found, ) + + def _on_create_review_submission_item_error(self, error_responses: ErrorResponse): + """ + Show informative warning message if some required fields are missing + for App Store Version localization whose locale matches with application's + default locale. + """ + + missing_attributes = self._get_missing_required_attributes_names(error_responses) + if not missing_attributes: + return + elif len(missing_attributes) == 1: + attribute_names = missing_attributes[0] + else: + attribute_names = " and ".join([", ".join(missing_attributes[:-1]), missing_attributes[-1]]) + + missing_default_locale_attributes_error = ( + f"\nCreating {ReviewSubmissionItem} failed. Please ensure that {AppStoreVersionLocalization} " + f"for your application default locale defines {attribute_names}!\n" + ) + self.echo(Colors.YELLOW(missing_default_locale_attributes_error)) + + @classmethod + def _get_missing_required_attributes_names(cls, error_responses: ErrorResponse) -> List[str]: + """ + Extract names of fields that are undefined for application's + default locale App Store Version localization + """ + default_locale_missing_values = set() + + for associated_error in error_responses.iter_associated_errors(): + if associated_error.code == "ENTITY_ERROR.ATTRIBUTE.REQUIRED" and associated_error.source_pointer: + # source pointer is like '/data/attributes/keywords' + asc_field_name = associated_error.source_pointer.split("/")[-1] + field_name = case_conversion.camel_to_snake(asc_field_name).replace("_", " ").lower() + default_locale_missing_values.add(field_name.replace(" url", " URL")) + + return list(default_locale_missing_values) diff --git a/src/codemagic/tools/_app_store_connect/action_groups/review_submissions_actions_group.py b/src/codemagic/tools/_app_store_connect/action_groups/review_submissions_actions_group.py index 3524c7da..a2dd9f2d 100644 --- a/src/codemagic/tools/_app_store_connect/action_groups/review_submissions_actions_group.py +++ b/src/codemagic/tools/_app_store_connect/action_groups/review_submissions_actions_group.py @@ -1,11 +1,13 @@ from __future__ import annotations from abc import ABCMeta +from typing import List from codemagic import cli from codemagic.apple.resources import Platform from codemagic.apple.resources import ResourceId from codemagic.apple.resources import ReviewSubmission +from codemagic.apple.resources import ReviewSubmissionItem from ..abstract_base_action import AbstractBaseAction from ..action_group import AppStoreConnectActionGroup @@ -95,3 +97,21 @@ def confirm_review_submission( should_print, submitted=True, ) + + @cli.action( + "items", + ReviewSubmissionArgument.REVIEW_SUBMISSION_ID, + action_group=AppStoreConnectActionGroup.REVIEW_SUBMISSIONS, + ) + def list_review_submission_items(self, review_submission_id: ResourceId) -> List[ReviewSubmissionItem]: + """ + List review submission items for specified review submission + """ + return self._list_related_resources( + resource_id=review_submission_id, + resource_type=ReviewSubmission, + related_resource_type=ReviewSubmissionItem, + list_related_resources_method=self.api_client.review_submissions.list_items, + resource_filter=None, + should_print=True, + ) diff --git a/src/codemagic/tools/_app_store_connect/arguments.py b/src/codemagic/tools/_app_store_connect/arguments.py index f6ed4065..c3386c27 100644 --- a/src/codemagic/tools/_app_store_connect/arguments.py +++ b/src/codemagic/tools/_app_store_connect/arguments.py @@ -672,6 +672,14 @@ class ReviewSubmissionArgument(cli.Argument): ) +class ReviewSubmissionItemArgument(cli.Argument): + REVIEW_SUBMISSION_ITEM_ID = cli.ArgumentProperties( + key="review_submission_item_id", + type=ResourceId, + description="UUID value of the review submission", + ) + + class AppStoreVersionLocalizationArgument(cli.Argument): APP_STORE_VERSION_LOCALIZATION_ID = cli.ArgumentProperties( key="app_store_version_localization_id", @@ -702,6 +710,16 @@ class AppStoreVersionLocalizationArgument(cli.Argument): "choices": list(Locale), }, ) + LOCALES = cli.ArgumentProperties.duplicate( + LOCALE, + key="locales", + flags=("--locale", "-l"), + argparse_kwargs={ + "nargs": "+", + "required": False, + "choices": list(Locale), + }, + ) DESCRIPTION = cli.ArgumentProperties( key="description", flags=("--description", "-d"), diff --git a/src/codemagic/tools/_app_store_connect/errors.py b/src/codemagic/tools/_app_store_connect/errors.py index 52851e2b..8fa21312 100644 --- a/src/codemagic/tools/_app_store_connect/errors.py +++ b/src/codemagic/tools/_app_store_connect/errors.py @@ -1,5 +1,15 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Optional + from codemagic import cli +if TYPE_CHECKING: + from codemagic.apple.resources import ErrorResponse + class AppStoreConnectError(cli.CliAppException): - pass + def __init__(self, *args, api_error_response: Optional[ErrorResponse] = None, **kwargs): + self.api_error = api_error_response + super().__init__(*args, **kwargs) diff --git a/src/codemagic/tools/_app_store_connect/resource_manager_mixin.py b/src/codemagic/tools/_app_store_connect/resource_manager_mixin.py index dd9e91bd..ebc64a5c 100644 --- a/src/codemagic/tools/_app_store_connect/resource_manager_mixin.py +++ b/src/codemagic/tools/_app_store_connect/resource_manager_mixin.py @@ -37,7 +37,10 @@ def _create_resource( try: resource = create_resource(**create_params) except AppStoreConnectApiError as api_error: - raise AppStoreConnectError(str(api_error)) from api_error + raise AppStoreConnectError( + str(api_error), + api_error_response=api_error.error_response, + ) from api_error self.printer.print_resource(resource, should_print) self.printer.log_created(resource) @@ -58,7 +61,10 @@ def _get_resource( try: resource = read_resource(resource_id) except AppStoreConnectApiError as api_error: - raise AppStoreConnectError(str(api_error)) + raise AppStoreConnectError( + str(api_error), + api_error_response=api_error.error_response, + ) from api_error self.printer.print_resource(resource, should_print) return resource @@ -77,7 +83,10 @@ def _list_resources( try: resources = list_resources(resource_filter=resource_filter) except AppStoreConnectApiError as api_error: - raise AppStoreConnectError(str(api_error)) + raise AppStoreConnectError( + str(api_error), + api_error_response=api_error.error_response, + ) from api_error if filter_predicate is not None: resources = list(filter(filter_predicate, resources)) @@ -99,7 +108,10 @@ def _get_related_resource( try: resource = read_related_resource_method(resource_id) except AppStoreConnectApiError as api_error: - raise AppStoreConnectError(str(api_error)) + raise AppStoreConnectError( + str(api_error), + api_error_response=api_error.error_response, + ) from api_error if resource is None: raise AppStoreConnectError(f"{related_resource_type} was not found for {resource_type} {resource_id}") @@ -115,6 +127,7 @@ def _list_related_resources( list_related_resources_method: Callable[..., List[R2]], resource_filter: Optional[ResourceManager.Filter], should_print: bool, + filter_predicate: Optional[Callable[[R2], bool]] = None, ) -> List[R2]: self.printer.log_get_related(related_resource_type, resource_type, resource_id) kwargs = {"resource_filter": resource_filter} if resource_filter else {} @@ -122,7 +135,13 @@ def _list_related_resources( try: resources = list_related_resources_method(resource_id, **kwargs) except AppStoreConnectApiError as api_error: - raise AppStoreConnectError(str(api_error)) + raise AppStoreConnectError( + str(api_error), + api_error_response=api_error.error_response, + ) from api_error + + if filter_predicate is not None: + resources = list(filter(filter_predicate, resources)) self.printer.log_found(related_resource_type, resources, resource_filter, resource_type, resource_id) self.printer.print_resources(resources, should_print) @@ -146,7 +165,10 @@ def _delete_resource( if ignore_not_found is True and api_error.status_code == 404: self.printer.log_ignore_not_deleted(resource_manager.resource_type, resource_id) else: - raise AppStoreConnectError(str(api_error)) + raise AppStoreConnectError( + str(api_error), + api_error_response=api_error.error_response, + ) from api_error else: self.printer.log_deleted(resource_manager.resource_type, resource_id) @@ -166,7 +188,10 @@ def _modify_resource( try: resource = modify_resource(resource_id, **update_params) except AppStoreConnectApiError as api_error: - raise AppStoreConnectError(str(api_error)) + raise AppStoreConnectError( + str(api_error), + api_error_response=api_error.error_response, + ) from api_error self.printer.log_modified(resource_manager.resource_type, resource_id) self.printer.print_resource(resource, should_print) diff --git a/src/codemagic/utilities/case_conversion.py b/src/codemagic/utilities/case_conversion.py new file mode 100644 index 00000000..c3f01c56 --- /dev/null +++ b/src/codemagic/utilities/case_conversion.py @@ -0,0 +1,9 @@ +import re + + +def snake_to_camel(snake_case: str) -> str: + return re.sub(r"_(\w)", lambda m: m.group(1).upper(), snake_case) + + +def camel_to_snake(camel_case: str) -> str: + return re.sub(r"([A-Z])", lambda m: f"_{m.group(1).lower()}", camel_case).lstrip("_") diff --git a/tests/apple/app_store_connect/test_resource_manager.py b/tests/apple/app_store_connect/test_resource_manager.py index c4c1de47..3ebeadff 100644 --- a/tests/apple/app_store_connect/test_resource_manager.py +++ b/tests/apple/app_store_connect/test_resource_manager.py @@ -43,20 +43,3 @@ class StubFilter(ResourceManager.Filter): def test_resource_manager_filter_to_params_conversion(filter_params, expected_query_params): test_filter = StubFilter(**filter_params) assert test_filter.as_query_params() == expected_query_params - - -@pytest.mark.parametrize( - "snake_case_input, expected_camel_case_output", - [ - ("", ""), - ("word", "word"), - ("_word", "Word"), - ("snake_case", "snakeCase"), - ("snake_case_", "snakeCase_"), - ("snake_case_ snakeCase", "snakeCase_ snakeCase"), - ("a_b_c_d_e_f", "aBCDEF"), - ], -) -def test_resource_manager_filter_camel_case_converter(snake_case_input, expected_camel_case_output): - converted_input = ResourceManager.Filter._snake_to_camel(snake_case_input) - assert converted_input == expected_camel_case_output diff --git a/tests/apple/resources/conftest.py b/tests/apple/resources/conftest.py index 692c1501..1a842a3b 100644 --- a/tests/apple/resources/conftest.py +++ b/tests/apple/resources/conftest.py @@ -47,6 +47,12 @@ def api_error_response() -> Dict: return json.loads(mock_path.read_text()) +@pytest.fixture +def api_error_response_entity_state_invalid() -> Dict: + mock_path = pathlib.Path(__file__).parent / "mocks" / "error_response_entity_state_invalid.json" + return json.loads(mock_path.read_text()) + + @pytest.fixture def api_error_response_with_links() -> Dict: mock_path = pathlib.Path(__file__).parent / "mocks" / "error_response_with_links.json" diff --git a/tests/apple/resources/mocks/error_response_entity_state_invalid.json b/tests/apple/resources/mocks/error_response_entity_state_invalid.json new file mode 100644 index 00000000..3d658fce --- /dev/null +++ b/tests/apple/resources/mocks/error_response_entity_state_invalid.json @@ -0,0 +1,47 @@ +{ + "errors": [ + { + "id": "6d516aab-7949-48e1-9205-89b9bc2e736b", + "status": "409", + "code": "STATE_ERROR.ENTITY_STATE_INVALID", + "title": "appStoreVersions with id '409ebefb-ebd1-4f1a-903d-6ba16e013ebf' is not in valid state.", + "detail": "This resource cannot be reviewed, please check associated errors to see why.", + "meta": { + "associatedErrors": { + "/v1/appStoreVersionLocalizations/253cf0c8-8057-49b7-be16-3b1cc78837e5": [ + { + "id": "00190291-bf3a-417d-903c-804640a7df60", + "status": "409", + "code": "ENTITY_ERROR.ATTRIBUTE.REQUIRED", + "title": "The provided entity is missing a required attribute", + "detail": "You must provide a value for the attribute 'description' with this request", + "source": { + "pointer": "/data/attributes/description" + } + }, + { + "id": "d5e83b1c-85b5-44ea-a9ba-5f2f3aa0cba0", + "status": "409", + "code": "ENTITY_ERROR.ATTRIBUTE.REQUIRED", + "title": "The provided entity is missing a required attribute", + "detail": "You must provide a value for the attribute 'keywords' with this request", + "source": { + "pointer": "/data/attributes/keywords" + } + }, + { + "id": "5d9f0f60-8791-4bed-bb98-435250599281", + "status": "409", + "code": "ENTITY_ERROR.ATTRIBUTE.REQUIRED", + "title": "The provided entity is missing a required attribute", + "detail": "You must provide a value for the attribute 'supportUrl' with this request", + "source": { + "pointer": "/data/attributes/supportUrl" + } + } + ] + } + } + } + ] +} diff --git a/tests/apple/resources/test_error_response.py b/tests/apple/resources/test_error_response.py index dd452579..04c3d9c8 100644 --- a/tests/apple/resources/test_error_response.py +++ b/tests/apple/resources/test_error_response.py @@ -11,3 +11,12 @@ def test_error_response_initialization(api_error_response): def test_error_response_with_links(api_error_response_with_links): error_response = ErrorResponse(api_error_response_with_links) assert error_response.dict() == api_error_response_with_links + + +def test_error_iter_associated_errors(api_error_response_entity_state_invalid): + error_response = ErrorResponse(api_error_response_entity_state_invalid) + associated_errors = list(error_response.iter_associated_errors()) + assert len(associated_errors) == 3 + assert associated_errors[0].source_pointer == "/data/attributes/description" + assert associated_errors[1].source_pointer == "/data/attributes/keywords" + assert associated_errors[2].source_pointer == "/data/attributes/supportUrl" diff --git a/tests/utilities/test_case_conversion.py b/tests/utilities/test_case_conversion.py new file mode 100644 index 00000000..c69c97ec --- /dev/null +++ b/tests/utilities/test_case_conversion.py @@ -0,0 +1,37 @@ +import pytest +from codemagic.utilities import case_conversion + + +@pytest.mark.parametrize( + ("snake_case_input", "expected_camel_case_output"), + [ + ("", ""), + ("word", "word"), + ("_word", "Word"), + ("snake_case", "snakeCase"), + ("snake_case_", "snakeCase_"), + ("snake_case_ snakeCase", "snakeCase_ snakeCase"), + ("a_b_c_d_e_f", "aBCDEF"), + ], +) +def test_snake_case_to_camel_case(snake_case_input, expected_camel_case_output): + converted_input = case_conversion.snake_to_camel(snake_case_input) + assert converted_input == expected_camel_case_output + + +@pytest.mark.parametrize( + ("camel_case_input", "expected_snake_case_output"), + [ + ("", ""), + ("word", "word"), + ("Word", "word"), + ("camelCase", "camel_case"), + ("camelCase_", "camel_case_"), + ("camelCase_ camelCase", "camel_case_ camel_case"), + ("camelCase snake_case", "camel_case snake_case"), + ("aBCDEF", "a_b_c_d_e_f"), + ], +) +def test_camel_case_to_snake_case(camel_case_input, expected_snake_case_output): + converted_input = case_conversion.camel_to_snake(camel_case_input) + assert converted_input == expected_snake_case_output