diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index e9bcc789bcb228ce18fca4a83c05c830e48f065e..6b5a828c00901a6dc62fdf0c2f2812f7a2023bb3 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -33,6 +33,7 @@ export const integrationFormSections = {
JIRA_ISSUES: 'jira_issues',
TRIGGER: 'trigger',
APPLE_APP_STORE: 'apple_app_store',
+ GOOGLE_PLAY: 'google_play',
};
export const integrationFormSectionComponents = {
@@ -42,6 +43,7 @@ export const integrationFormSectionComponents = {
[integrationFormSections.JIRA_ISSUES]: 'IntegrationSectionJiraIssues',
[integrationFormSections.TRIGGER]: 'IntegrationSectionTrigger',
[integrationFormSections.APPLE_APP_STORE]: 'IntegrationSectionAppleAppStore',
+ [integrationFormSections.GOOGLE_PLAY]: 'IntegrationSectionGooglePlay',
};
export const integrationTriggerEvents = {
diff --git a/app/assets/javascripts/integrations/edit/components/integration_forms/section.vue b/app/assets/javascripts/integrations/edit/components/integration_forms/section.vue
index d33f0ba317110dca154abfa909bb22255405f8e4..5335b7b6ee2c491bbcb51d20f21b45e1c9c5808e 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_forms/section.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_forms/section.vue
@@ -32,6 +32,10 @@ export default {
import(
/* webpackChunkName: 'IntegrationSectionAppleAppStore' */ '~/integrations/edit/components/sections/apple_app_store.vue'
),
+ IntegrationSectionGooglePlay: () =>
+ import(
+ /* webpackChunkName: 'IntegrationSectionGooglePlay' */ '~/integrations/edit/components/sections/google_play.vue'
+ ),
},
directives: {
SafeHtml,
diff --git a/app/assets/javascripts/integrations/edit/components/sections/google_play.vue b/app/assets/javascripts/integrations/edit/components/sections/google_play.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3094e24241a62d852bf10b2e3917fc65cb947a52
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/sections/google_play.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/controllers/concerns/integrations/params.rb b/app/controllers/concerns/integrations/params.rb
index b8b93814e2b69794db53b6d4e175114e28b938be..7e1ba49d442d072a3304c08ed5cd300a08fef460 100644
--- a/app/controllers/concerns/integrations/params.rb
+++ b/app/controllers/concerns/integrations/params.rb
@@ -73,6 +73,8 @@ module Params
:server,
:server_host,
:server_port,
+ :service_account_key,
+ :service_account_key_file_name,
:sound,
:subdomain,
:teamcity_url,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 0941f6d4cf0c82ab89275853dfcdc2c58ce62b61..0c2332d801208c21bcdc58d297c89edac7a89395 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -71,6 +71,7 @@ class Build < Ci::Processable
delegate :gitlab_deploy_token, to: :project
delegate :harbor_integration, to: :project
delegate :apple_app_store_integration, to: :project
+ delegate :google_play_integration, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
delegate :ensure_persistent_ref, to: :pipeline
delegate :enable_debug_trace!, to: :metadata
@@ -599,6 +600,7 @@ def persisted_variables
.concat(deploy_token_variables)
.concat(harbor_variables)
.concat(apple_app_store_variables)
+ .concat(google_play_variables)
end
end
@@ -649,6 +651,13 @@ def apple_app_store_variables
Gitlab::Ci::Variables::Collection.new(apple_app_store_integration.ci_variables)
end
+ def google_play_variables
+ return [] unless google_play_integration.try(:activated?)
+ return [] unless pipeline.protected_ref?
+
+ Gitlab::Ci::Variables::Collection.new(google_play_integration.ci_variables)
+ end
+
def features
{
trace_sections: true,
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 8bef8b08c196acbd42b656e3cd17a8a5a75ced76..6aa3ef526705a517faac824588d97094867c92fc 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -27,7 +27,7 @@ class Integration < ApplicationRecord
# TODO Shimo is temporary disabled on group and instance-levels.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/345677
PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[
- apple_app_store jenkins shimo
+ apple_app_store google_play jenkins shimo
].freeze
# Fake integrations to help with local development.
diff --git a/app/models/integrations/google_play.rb b/app/models/integrations/google_play.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8f1d2e7e1ecd08603d2dec1ecde74c982415a846
--- /dev/null
+++ b/app/models/integrations/google_play.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Integrations
+ class GooglePlay < Integration
+ SECTION_TYPE_GOOGLE_PLAY = 'google_play'
+
+ with_options if: :activated? do
+ validates :service_account_key, presence: true, json_schema: {
+ filename: "google_service_account_key", parse_json: true
+ }
+ validates :service_account_key_file_name, presence: true
+ end
+
+ field :service_account_key_file_name,
+ section: SECTION_TYPE_CONNECTION,
+ required: true,
+ is_secret: false
+
+ field :service_account_key, api_only: true, is_secret: false
+
+ def title
+ s_('GooglePlay|Google Play')
+ end
+
+ def description
+ s_('GooglePlay|Use GitLab to build and release an app in Google Play.')
+ end
+
+ def help
+ variable_list = [
+ 'SUPPLY_JSON_KEY_DATA'
+ ]
+
+ # rubocop:disable Layout/LineLength
+ texts = [
+ s_("Use the Google Play integration to connect to Google Play with fastlane in CI/CD pipelines."),
+ s_("After you enable the integration, the following protected variable is created for CI/CD use:"),
+ variable_list.join('
'),
+ s_(format("To generate a Google Play service account key and use this integration, see the integration documentation.", url: "#")).html_safe
+ ]
+ # rubocop:enable Layout/LineLength
+
+ texts.join('
'.html_safe)
+ end
+
+ def self.to_param
+ 'google_play'
+ end
+
+ def self.supported_events
+ []
+ end
+
+ def sections
+ [
+ {
+ type: SECTION_TYPE_GOOGLE_PLAY,
+ title: s_('Integrations|Integration details'),
+ description: help
+ }
+ ]
+ end
+
+ def test(*_args)
+ client.fetch_access_token!
+ { success: true }
+ rescue Signet::AuthorizationError => error
+ { success: false, message: error }
+ end
+
+ def ci_variables
+ return [] unless activated?
+
+ [
+ { key: 'SUPPLY_JSON_KEY_DATA', value: service_account_key, masked: true, public: false }
+ ]
+ end
+
+ private
+
+ def client
+ Google::Auth::ServiceAccountCredentials.make_creds(
+ json_key_io: StringIO.new(service_account_key),
+ scope: ['https://www.googleapis.com/auth/androidpublisher']
+ )
+ end
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index b1bb0ff2bbb7ec09d33c0e142a17f403ea17628b..c152c83c6a74209093a54990a4f515f31ebb1133 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -188,6 +188,7 @@ def self.integration_association_name(name)
has_one :emails_on_push_integration, class_name: 'Integrations::EmailsOnPush'
has_one :ewm_integration, class_name: 'Integrations::Ewm'
has_one :external_wiki_integration, class_name: 'Integrations::ExternalWiki'
+ has_one :google_play_integration, class_name: 'Integrations::GooglePlay'
has_one :hangouts_chat_integration, class_name: 'Integrations::HangoutsChat'
has_one :harbor_integration, class_name: 'Integrations::Harbor'
has_one :irker_integration, class_name: 'Integrations::Irker'
@@ -1637,6 +1638,7 @@ def find_or_initialize_integrations
def disabled_integrations
disabled_integrations = []
disabled_integrations << 'apple_app_store' unless Feature.enabled?(:apple_app_store_integration, self)
+ disabled_integrations << 'google_play' unless Feature.enabled?(:google_play_integration, self)
disabled_integrations
end
diff --git a/app/validators/json_schema_validator.rb b/app/validators/json_schema_validator.rb
index 4896c2ea2efbab2800a117b487009c2e68bf1582..9c246a114f64d2fd966bffefc466e73237fb08ce 100644
--- a/app/validators/json_schema_validator.rb
+++ b/app/validators/json_schema_validator.rb
@@ -25,6 +25,7 @@ def initialize(options)
def validate_each(record, attribute, value)
value = value.to_h.stringify_keys if options[:hash_conversion] == true
+ value = Gitlab::Json.parse(value.to_s) if options[:parse_json] == true && !value.nil?
unless valid_schema?(value)
record.errors.add(attribute, _("must be a valid json schema"))
diff --git a/app/validators/json_schemas/google_service_account_key.json b/app/validators/json_schemas/google_service_account_key.json
new file mode 100644
index 0000000000000000000000000000000000000000..d040ef19f66f6a77a6969952e198adf2c3fa4f3d
--- /dev/null
+++ b/app/validators/json_schemas/google_service_account_key.json
@@ -0,0 +1,48 @@
+{
+ "description": "Google service account key",
+ "type": "object",
+ "required": [
+ "type",
+ "project_id",
+ "private_key_id",
+ "private_key",
+ "client_email",
+ "client_id",
+ "auth_uri",
+ "token_uri",
+ "auth_provider_x509_cert_url",
+ "client_x509_cert_url"
+ ],
+ "properties": {
+ "type": {
+ "const": "service_account"
+ },
+ "project_id": {
+ "type": "string"
+ },
+ "private_key_id": {
+ "type": "string"
+ },
+ "private_key": {
+ "type": "string"
+ },
+ "client_email": {
+ "type": "string"
+ },
+ "client_id": {
+ "type": "string"
+ },
+ "auth_uri": {
+ "type": "string"
+ },
+ "token_uri": {
+ "type": "string"
+ },
+ "auth_provider_x509_cert_url": {
+ "type": "string"
+ },
+ "client_x509_cert_url": {
+ "type": "string"
+ }
+ }
+}
diff --git a/config/feature_flags/development/google_play_integration.yml b/config/feature_flags/development/google_play_integration.yml
new file mode 100644
index 0000000000000000000000000000000000000000..81c509cdab7583437e694edb7ab51802de6bfb9b
--- /dev/null
+++ b/config/feature_flags/development/google_play_integration.yml
@@ -0,0 +1,8 @@
+---
+name: google_play_integration
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110440
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389611
+milestone: '15.10'
+type: development
+group: group::incubation
+default_enabled: false
diff --git a/config/metrics/counts_all/20230210184724_projects_inheriting_google_play_active.yml b/config/metrics/counts_all/20230210184724_projects_inheriting_google_play_active.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9a24543390a6be21c926b7f4b8760e7fcedd7cf0
--- /dev/null
+++ b/config/metrics/counts_all/20230210184724_projects_inheriting_google_play_active.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.projects_inheriting_google_play_active
+description: Count of active projects inheriting integrations for Google Play
+product_section: dev
+product_stage: manage
+product_group: integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "15.10"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20230222192643_projects_google_play_active.yml b/config/metrics/counts_all/20230222192643_projects_google_play_active.yml
new file mode 100644
index 0000000000000000000000000000000000000000..712a9b9cac1540802f67f673c5ac9c9dc6524523
--- /dev/null
+++ b/config/metrics/counts_all/20230222192643_projects_google_play_active.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.projects_google_play_active
+description: Count of projects with active integrations for Google Play
+product_section: dev
+product_stage: manage
+product_group: integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "15.10"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20230222193011_instances_google_play_active.yml b/config/metrics/counts_all/20230222193011_instances_google_play_active.yml
new file mode 100644
index 0000000000000000000000000000000000000000..00f99ed13f4a4203a299f5444858b042ff2ebcea
--- /dev/null
+++ b/config/metrics/counts_all/20230222193011_instances_google_play_active.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.instances_google_play_active
+description: Count of instances with active integrations for Google Play
+product_section: dev
+product_stage: manage
+product_group: integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "15.10"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20230222193151_groups_inheriting_google_play_active.yml b/config/metrics/counts_all/20230222193151_groups_inheriting_google_play_active.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1dad27560b69ce1e0e061f4951a4cedddd2058e3
--- /dev/null
+++ b/config/metrics/counts_all/20230222193151_groups_inheriting_google_play_active.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.groups_inheriting_google_play_active
+description: Count of active groups inheriting integrations for Google Play
+product_section: dev
+product_stage: manage
+product_group: integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "15.10"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20230222193255_groups_google_play_active.yml b/config/metrics/counts_all/20230222193255_groups_google_play_active.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fe83398f9ecb0da2a1ef72561235dd053137f0d3
--- /dev/null
+++ b/config/metrics/counts_all/20230222193255_groups_google_play_active.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.groups_google_play_active
+description: Count of active groups inheriting integrations for Google Play
+product_section: dev
+product_stage: manage
+product_group: integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "15.10"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/db/docs/integrations.yml b/db/docs/integrations.yml
index 5bb4f4485417510a48759344558afd0583aaeb74..c1b18b29804ea935df39d1069c3c62bfee38ca47 100644
--- a/db/docs/integrations.yml
+++ b/db/docs/integrations.yml
@@ -26,6 +26,7 @@ classes:
- Integrations::ExternalWiki
- Integrations::Github
- Integrations::GitlabSlackApplication
+- Integrations::GooglePlay
- Integrations::HangoutsChat
- Integrations::Harbor
- Integrations::Irker
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index a4696acdfb33fdcd07a3a1d9be624da3b3e08cdf..5dd3886d5c52f791763dd8fff3783c68aae441be 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -23597,6 +23597,7 @@ State of a Sentry error.
| `EXTERNAL_WIKI_SERVICE` | ExternalWikiService type. |
| `GITHUB_SERVICE` | GithubService type. |
| `GITLAB_SLACK_APPLICATION_SERVICE` | GitlabSlackApplicationService type (Gitlab.com only). |
+| `GOOGLE_PLAY_SERVICE` | GooglePlayService type. |
| `HANGOUTS_CHAT_SERVICE` | HangoutsChatService type. |
| `HARBOR_SERVICE` | HarborService type. |
| `IRKER_SERVICE` | IrkerService type. |
diff --git a/doc/user/project/integrations/google_play.md b/doc/user/project/integrations/google_play.md
new file mode 100644
index 0000000000000000000000000000000000000000..553e82be3828cc931590e69c0621e3a4f2e4caf3
--- /dev/null
+++ b/doc/user/project/integrations/google_play.md
@@ -0,0 +1,49 @@
+---
+stage: Manage
+group: Integrations
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Google Play **(FREE)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621) in GitLab 15.10 [with a flag](../../../administration/feature_flags.md) named `google_play_integration`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `google_play_integration`. On GitLab.com, this feature is not available.
+
+With the Google Play integration, you can configure your CI/CD pipelines to connect to the [Google Play Console](https://play.google.com/console) to build and release apps for Android devices.
+
+The Google Play integration works out of the box with [fastlane](https://fastlane.tools/). You can also use this integration with other build tools.
+
+## Enable the integration in GitLab
+
+Prerequisites:
+
+- You must have a [Google Play Console](https://play.google.com/console/signup) developer account.
+- You must [generate a new service account key for your project](https://developers.google.com/android-publisher/getting_started) from the Google Cloud console.
+
+To enable the Google Play integration in GitLab:
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select **Settings > Integrations**.
+1. Select **Google Play**.
+1. In **Enable Integration**, select the **Active** checkbox.
+1. In **Service account key (.JSON)**, drag or upload your key file.
+1. Select **Save changes**.
+
+After you enable the integration, the global variable `$SUPPLY_JSON_KEY_DATA` is created for CI/CD use.
+
+### CI/CD variable security
+
+Malicious code pushed to your `.gitlab-ci.yml` file could compromise your variables, including `$SUPPLY_JSON_KEY_DATA`, and send them to a third-party server. For more information, see [CI/CD variable security](../../../ci/variables/index.md#cicd-variable-security).
+
+## Enable the integration in fastlane
+
+To enable the integration in fastlane and upload the build to the given track in Google Play, you can add the following code to your app's `fastlane/Fastfile`:
+
+```ruby
+upload_to_play_store(
+ track: 'internal',
+ aab: '../build/app/outputs/bundle/release/app-release.aab'
+)
+```
diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb
index d13a78db01cd0ca05d23ceed15b744c919f7c5cf..60cb61ef02f74403278dfdce8acc159f400ebdbf 100644
--- a/lib/api/helpers/integrations_helpers.rb
+++ b/lib/api/helpers/integrations_helpers.rb
@@ -453,6 +453,20 @@ def self.integrations
desc: 'The URL of the external wiki'
}
],
+ 'google-play' => [
+ {
+ required: true,
+ name: :service_account_key,
+ type: String,
+ desc: 'The Google Play Service Account Key'
+ },
+ {
+ required: true,
+ name: :service_account_key_file_name,
+ type: String,
+ desc: 'The Google Play Service Account Key File Name'
+ }
+ ],
'hangouts-chat' => [
{
required: true,
@@ -924,6 +938,7 @@ def self.integration_classes
::Integrations::EmailsOnPush,
::Integrations::Ewm,
::Integrations::ExternalWiki,
+ ::Integrations::GooglePlay,
::Integrations::HangoutsChat,
::Integrations::Harbor,
::Integrations::Irker,
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 85484b1505e0e91f12e02553cf2f13d26e3b0c6a..2ad9741e3e8c4be7dbe6fbbcdaf4e86911a8a507 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3705,6 +3705,9 @@ msgstr ""
msgid "After the export is complete, download the data file from a notification email or from this page. You can then import the data file from the %{strong_text_start}Create new group%{strong_text_end} page of another GitLab instance."
msgstr ""
+msgid "After you enable the integration, the following protected variable is created for CI/CD use:"
+msgstr ""
+
msgid "After you've reviewed these contribution guidelines, you'll be all set to"
msgstr ""
@@ -19649,6 +19652,30 @@ msgstr ""
msgid "GoogleCloud|Revoke authorizations granted to GitLab. This does not invalidate service accounts."
msgstr ""
+msgid "GooglePlay|Drag your key file here or %{linkStart}click to upload%{linkEnd}."
+msgstr ""
+
+msgid "GooglePlay|Drag your key file to start the upload."
+msgstr ""
+
+msgid "GooglePlay|Error: The file you're trying to upload is not a service account key."
+msgstr ""
+
+msgid "GooglePlay|Google Play"
+msgstr ""
+
+msgid "GooglePlay|Leave empty to use your current service account key."
+msgstr ""
+
+msgid "GooglePlay|Service account key (.json)"
+msgstr ""
+
+msgid "GooglePlay|Upload a new service account key (replace %{currentFileName})"
+msgstr ""
+
+msgid "GooglePlay|Use GitLab to build and release an app in Google Play."
+msgstr ""
+
msgid "Got it"
msgstr ""
@@ -46426,6 +46453,9 @@ msgstr ""
msgid "Use the Apple App Store Connect integration to easily connect to the Apple App Store with Fastlane in CI/CD pipelines."
msgstr ""
+msgid "Use the Google Play integration to connect to Google Play with fastlane in CI/CD pipelines."
+msgstr ""
+
msgid "Use the link below to confirm your email address (%{email})"
msgstr ""
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index dccca9ce30decf09af91e5a97466da15ac199541..5a666e57ad1860656bb439d186d7004aea87b491 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -265,6 +265,15 @@
app_store_private_key { File.read('spec/fixtures/auth_key.p8') }
end
+ factory :google_play_integration, class: 'Integrations::GooglePlay' do
+ project
+ active { true }
+ type { 'Integrations::GooglePlay' }
+
+ service_account_key_file_name { 'service_account.json' }
+ service_account_key { File.read('spec/fixtures/service_account.json') }
+ end
+
# this is for testing storing values inside properties, which is deprecated and will be removed in
# https://gitlab.com/gitlab-org/gitlab/issues/29404
trait :without_properties_callback do
diff --git a/spec/features/projects/integrations/google_play_spec.rb b/spec/features/projects/integrations/google_play_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5db4bc8809fa5ca834fedf1876f58ea29cc1e34c
--- /dev/null
+++ b/spec/features/projects/integrations/google_play_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Upload Dropzone Field', feature_category: :integrations do
+ include_context 'project integration activation'
+
+ it 'uploads the file data to the correct form fields and updates the messaging correctly', :js, :aggregate_failures do
+ visit_project_integration('Google Play')
+
+ expect(page).to have_content('Drag your key file here or click to upload.')
+ expect(page).not_to have_content('service_account.json')
+
+ find("input[name='service[dropzone_file_name]']",
+ visible: false).set(Rails.root.join('spec/fixtures/service_account.json'))
+
+ expect(find("input[name='service[service_account_key]']",
+ visible: false).value).to eq(File.read(Rails.root.join('spec/fixtures/service_account.json')))
+ expect(find("input[name='service[service_account_key_file_name]']",
+ visible: false).value).to eq('service_account.json')
+
+ expect(page).not_to have_content('Drag your key file here or click to upload.')
+ expect(page).to have_content('service_account.json')
+ end
+end
diff --git a/spec/fixtures/service_account.json b/spec/fixtures/service_account.json
new file mode 100644
index 0000000000000000000000000000000000000000..9f7f5526cf52643dae265f1e118641c2df4a0291
--- /dev/null
+++ b/spec/fixtures/service_account.json
@@ -0,0 +1,12 @@
+{
+ "type": "service_account",
+ "project_id": "demo-app-123",
+ "private_key_id": "47f0b1700983da548af6fcd37007f42996099999",
+ "private_key": "-----BEGIN PRIVATE KEY-----\nABCDEFIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJn8w20WcN+fi5\nIhO1BEFCv7ExK8J5rW5Pc8XpJgpQoL5cfv6qC6aS+x4maI7S4AG7diqXBLCfjlnA\nqBzXwCRnnPtQhu+v1ehAj5fGNa7F51f9aacRNmKdHzNmWZEPDuLqq0I/Ewcsotu+\nnb+tCYk1o2ahyPZau8JtXFZs7oZb7SrfgoSJemccxeVreGm1Dt6SM74/3qJAeHN/\niK/v0IiQP1GS4Jxgz38XQGo+jiTpNrFcf4S0RNxKcNf+tuuEBDi57LBLwdotM7E5\nF1l9pZZMWkmQKQIxeER6+2HuE56V6QPITwkQ/u9XZFQSgl4SBIw2sHr5D/xaUxjw\n+kMy2Jt9AgMBAAECggEACL7E34rRIWbP043cv3ZQs1RiWzY2mvWmCiMEzkz0rRRv\nyqNv0yXVYtzVV7KjdpY56leLgjM1Sv0PEQoUUtpWFJAXSXdKLaewSXPrpXCoz5OD\nekMgeItnQcE7nECdyAKsCSQw/SXg4t4p0a3WGsCwt3If2TwWIrov9R4zGcn1wMZn\n922WtZDmh2NqdTZIKElWZLxNlIr/1v88mAp7oSa1DLfqWkwEEnxK7GGAiwN8ARIF\nkvgiuKdsHBf5aNKg70xN6AcZx/Z4+KZxXxyKKF5VkjCtDzA97EjJqftDPwGTkela\n2bgiDSJs0Un0wQpFFRDrlfyo7rr9Ey/Gf4rR66NWeQKBgQD7qPP55xoWHCDvoK9P\nMN67qFLNDPWcKVKr8siwUlZ6/+acATXjfNUjsJLM7vBxYLjdtFxQ/vojJTQyMxHt\n80wARDk1DTu2zhltL2rKo6LfbwjQsot1MLZFXAMwqtHTLfURaj8kO1JDV/j+4a94\nP0gzNMiBYAKWm6z08akEz2TrhQKBgQDNGfFvtxo4Mf6AA3iYXCwc0CJXb+cqZkW/\n7glnV+vDqYVo23HJaKHFD+Xqaj+cUrOUNglWgT9WSCZR++Hzw1OCPZvX2V9Z6eQh\ngqOBX6D19q9jfShfxLywEAD5pk7LMINumsNm6H+6shJQK5c67bsM9/KQbSnIlWhw\n7JBe8OlFmQKBgQDREyF2mb/7ZG0ch8N9qB0zjHkV79FRZqdPQUnn6s/8KgO90eei\nUkCFARpE9bF+kBul3UTg6aSIdE0z82fO51VZ11Qrtg3JJtrK8hznsyEKPaX2NI9V\n0h1r7DCeSxw9NS4nxLwmbr4+QqUTpA3yeaiTGiQGD+y2kSkU6nxACclPPQKBgFkb\nkVqg6YJKrjB90ZIYUY3/GzxzwLIaFumpCGretu6eIvkIhiokDExqeNBccuB+ych1\npZ7wrkzVMdjinythzFFEZQXlSdjtlhC9Cj52Bp92GoMV6EmbVwMDIPlVuNvsat3N\n3WFDV+ML5IryNVUD3gVnX/pBgyrDRsnw7VRiRGbZAoGBANxZwGKZo0zpyb5O5hS6\nxVrgJtIySlV5BOEjFXKeLwzByht8HmrHhSWix6WpPejfK1RHhl3boU6t9yeC0cre\nvUI/Y9LBhHXjSwWCWlqVe9yYqsde+xf0UYRS8IoaoJjus7YVJr9yPpCboEF28ZmQ\ndVBlpZYg6oLIar6waaLMz/1B\n-----END PRIVATE KEY-----\n",
+ "client_email": "demo-app-account@demo-app-374914.iam.gserviceaccount.com",
+ "client_id": "111111116847110173051",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.googleapis.com/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/demo-app-account%40demo-app-374914.iam.gserviceaccount.com"
+}
diff --git a/spec/frontend/integrations/edit/components/sections/google_play_spec.js b/spec/frontend/integrations/edit/components/sections/google_play_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..c0d6d17f639edd0acba913e52cb6b960f8464bf1
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/sections/google_play_spec.js
@@ -0,0 +1,54 @@
+import { shallowMount } from '@vue/test-utils';
+
+import IntegrationSectionGooglePlay from '~/integrations/edit/components/sections/google_play.vue';
+import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue';
+import { createStore } from '~/integrations/edit/store';
+
+describe('IntegrationSectionGooglePlay', () => {
+ let wrapper;
+
+ const createComponent = (fileName = '') => {
+ const store = createStore({
+ customState: {
+ fields: [
+ {
+ name: 'service_account_key_file_name',
+ value: fileName,
+ },
+ ],
+ },
+ });
+
+ wrapper = shallowMount(IntegrationSectionGooglePlay, {
+ store,
+ });
+ };
+
+ const findUploadDropzoneField = () => wrapper.findComponent(UploadDropzoneField);
+
+ describe('computed properties', () => {
+ it('renders UploadDropzoneField with default values', () => {
+ createComponent();
+
+ const field = findUploadDropzoneField();
+
+ expect(field.exists()).toBe(true);
+ expect(field.props()).toMatchObject({
+ label: 'Service account key (.json)',
+ helpText: '',
+ });
+ });
+
+ it('renders UploadDropzoneField with custom values for an attached file', () => {
+ createComponent('fileName.txt');
+
+ const field = findUploadDropzoneField();
+
+ expect(field.exists()).toBe(true);
+ expect(field.props()).toMatchObject({
+ label: 'Upload a new service account key (replace fileName.txt)',
+ helpText: 'Leave empty to use your current service account key.',
+ });
+ });
+ });
+});
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 056dc1f1966386864710e05740117f34c1122cac..490be954792effef68cc34b5cef9fc4e9185d70a 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -532,6 +532,7 @@ project:
- discord_integration
- drone_ci_integration
- emails_on_push_integration
+- google_play_integration
- pipelines_email_integration
- mattermost_slash_commands_integration
- shimo_integration
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 492768df14e96a7fc22eb35eb625e42f3dd9ea4e..99c168de6165f48a64200092d35a32e45ea9fa25 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -3639,6 +3639,46 @@
end
end
+ context 'for the google_play integration' do
+ let_it_be(:google_play_integration) { create(:google_play_integration) }
+
+ let(:google_play_variables) do
+ [
+ { key: 'SUPPLY_JSON_KEY_DATA', value: google_play_integration.service_account_key, masked: true, public: false }
+ ]
+ end
+
+ context 'when the google_play integration exists' do
+ context 'when a build is protected' do
+ before do
+ allow(build.pipeline).to receive(:protected_ref?).and_return(true)
+ build.project.update!(google_play_integration: google_play_integration)
+ end
+
+ it 'includes google_play variables' do
+ is_expected.to include(*google_play_variables)
+ end
+ end
+
+ context 'when a build is not protected' do
+ before do
+ allow(build.pipeline).to receive(:protected_ref?).and_return(false)
+ build.project.update!(google_play_integration: google_play_integration)
+ end
+
+ it 'does not include the google_play variable' do
+ expect(subject[:key] == 'SUPPLY_JSON_KEY_DATA').to eq(false)
+ end
+ end
+ end
+
+ context 'when the googel_play integration does not exist' do
+ it 'does not include google_play variable' do
+ expect(subject[:key] == 'SUPPLY_JSON_KEY_DATA').to eq(false)
+ end
+ end
+ end
+
context 'when build has dependency which has dotenv variable' do
let!(:prepare) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: [prepare.name] }) }
diff --git a/spec/models/integrations/google_play_spec.rb b/spec/models/integrations/google_play_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ab1aaad24e7be31d6e3717419e0e18749f308a9e
--- /dev/null
+++ b/spec/models/integrations/google_play_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::GooglePlay, feature_category: :mobile_devops do
+ describe 'Validations' do
+ context 'when active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of :service_account_key_file_name }
+ it { is_expected.to validate_presence_of :service_account_key }
+ it { is_expected.to allow_value(File.read('spec/fixtures/service_account.json')).for(:service_account_key) }
+ it { is_expected.not_to allow_value(File.read('spec/fixtures/group.json')).for(:service_account_key) }
+ end
+ end
+
+ context 'when integration is enabled' do
+ let(:google_play_integration) { build(:google_play_integration) }
+
+ describe '#fields' do
+ it 'returns custom fields' do
+ expect(google_play_integration.fields.pluck(:name)).to match_array(%w[service_account_key
+ service_account_key_file_name])
+ end
+ end
+
+ describe '#test' do
+ it 'returns true for a successful request' do
+ allow(Google::Auth::ServiceAccountCredentials).to receive_message_chain(:make_creds, :fetch_access_token!)
+ expect(google_play_integration.test[:success]).to be true
+ end
+
+ it 'returns false for an invalid request' do
+ allow(Google::Auth::ServiceAccountCredentials).to receive_message_chain(:make_creds,
+ :fetch_access_token!).and_raise(Signet::AuthorizationError.new('error'))
+ expect(google_play_integration.test[:success]).to be false
+ end
+ end
+
+ describe '#help' do
+ it 'renders prompt information' do
+ expect(google_play_integration.help).not_to be_empty
+ end
+ end
+
+ describe '.to_param' do
+ it 'returns the name of the integration' do
+ expect(described_class.to_param).to eq('google_play')
+ end
+ end
+
+ describe '#ci_variables' do
+ let(:google_play_integration) { build_stubbed(:google_play_integration) }
+
+ it 'returns vars when the integration is activated' do
+ ci_vars = [
+ {
+ key: 'SUPPLY_JSON_KEY_DATA',
+ value: google_play_integration.service_account_key,
+ masked: true,
+ public: false
+ }
+ ]
+
+ expect(google_play_integration.ci_variables).to match_array(ci_vars)
+ end
+ end
+ end
+
+ context 'when integration is disabled' do
+ let(:google_play_integration) { build_stubbed(:google_play_integration, active: false) }
+
+ describe '#ci_variables' do
+ it 'returns an empty array' do
+ expect(google_play_integration.ci_variables).to match_array([])
+ end
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 9db1dde6294682efa098c9535fdd69c460e3c28d..346948bb0263ea350d566a61be89a80e5a75d9df 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -50,6 +50,7 @@
it { is_expected.to have_one(:packagist_integration) }
it { is_expected.to have_one(:pushover_integration) }
it { is_expected.to have_one(:apple_app_store_integration) }
+ it { is_expected.to have_one(:google_play_integration) }
it { is_expected.to have_one(:asana_integration) }
it { is_expected.to have_many(:boards) }
it { is_expected.to have_one(:campfire_integration) }
diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
index 178dad627eda6a649ecf1c87d2a47d7248cccd18..f07e4f6c9c37a542b4640eaabb6def954e81ef42 100644
--- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
@@ -76,6 +76,10 @@
hash.merge!(k => 'ABC1')
elsif integration == 'apple_app_store' && k == :app_store_private_key_file_name
hash.merge!(k => 'ssl_key.pem')
+ elsif integration == 'google_play' && k == :service_account_key
+ hash.merge!(k => File.read('spec/fixtures/service_account.json'))
+ elsif integration == 'google_play' && k == :service_account_key_file_name
+ hash.merge!(k => 'service_account.json')
else
hash.merge!(k => "someword")
end