Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ _None_

### New Features

_None_
- Update `upload_build_to_apps_cdn` action with newly-added `install_type` and `sha` API parameters. [#651]

### Bug Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class UploadBuildToAppsCdnAction < Action
VALID_POST_STATUS = %w[publish draft].freeze
VALID_BUILD_TYPES = %w[Alpha Beta Nightly Production Prototype].freeze
VALID_PLATFORMS = ['Android', 'iOS', 'Mac - Silicon', 'Mac - Intel', 'Mac - Any', 'Windows'].freeze
VALID_INSTALL_TYPES = ['Full Install', 'Update'].freeze

def self.run(params)
UI.message('Uploading build to Apps CDN...')
Expand All @@ -36,11 +37,13 @@ def self.run(params)
visibility: params[:visibility].to_s.capitalize,
platform: params[:platform],
resource_type: RESOURCE_TYPE,
install_type: params[:install_type],
version: params[:version],
build_number: params[:build_number], # Optional: may be nil
minimum_system_version: params[:minimum_system_version], # Optional: may be nil
post_status: params[:post_status], # Optional: may be nil
release_notes: params[:release_notes], # Optional: may be nil
sha: params[:sha], # Optional: may be nil
error_on_duplicate: params[:error_on_duplicate] # defaults to false
}.compact
request_body, content_type = build_multipart_request(parameters: parameters, file_path: file_path)
Expand Down Expand Up @@ -210,6 +213,15 @@ def self.available_options
UI.user_error!("Build type must be one of: #{VALID_BUILD_TYPES.join(', ')}") unless VALID_BUILD_TYPES.include?(value)
end
),
FastlaneCore::ConfigItem.new(
key: :install_type,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

description: "The install type for the build. One of: #{VALID_INSTALL_TYPES.join(', ')}",
default_value: 'Full Install',
type: String,
verify_block: proc do |value|
UI.user_error!("Install type must be one of: #{VALID_INSTALL_TYPES.join(', ')}") unless VALID_INSTALL_TYPES.include?(value)
end
),
FastlaneCore::ConfigItem.new(
key: :visibility,
description: 'The visibility of the build (:internal or :external)',
Expand Down Expand Up @@ -256,6 +268,12 @@ def self.available_options
optional: true,
type: String
),
FastlaneCore::ConfigItem.new(
key: :sha,
description: 'A string representing the release, e.g. the most recent commit hash, cryptographic token, etc',
optional: true,
type: String
),
FastlaneCore::ConfigItem.new(
key: :error_on_duplicate,
description: 'If true, the action will error if a build matching the same metadata already exists. If false, any potential existing build matching the same metadata will be updated to replace the build with the new file',
Expand Down
145 changes: 103 additions & 42 deletions spec/upload_build_to_apps_cdn_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

describe Fastlane::Actions::UploadBuildToAppsCdnAction do
let(:test_site_id) { '12345678' }
let(:api_url) { "https://public-api.wordpress.com/rest/v1.1/sites/#{test_site_id}/media/new" }
let(:test_api_token) { 'test_api_token' }
let(:test_product) { 'WordPress.com Studio' }
let(:test_build_type) { 'Beta' }
Expand All @@ -20,50 +21,55 @@
let(:test_mime_type) { 'application/zip' }
let(:test_filename) { 'test_app.zip' }
let(:test_file_content) { 'test app binary' }
let(:test_sha) { 'badc0ffeebadf00d' }
let(:test_boundary) { '----WebKitFormBoundarydabad0001234dabad000' }
let(:stub_success_response) do
{
media: [
{
ID: test_media_id,
URL: test_media_url,
date: test_date,
mime_type: test_mime_type,
file: test_filename,
post_ID: test_post_id
},
]
}.to_json
end

before do
WebMock.disable_net_connect!
allow(SecureRandom).to receive(:hex).with(10).and_return('dabad0001234dabad000')
end

after do
WebMock.allow_net_connect!
end

# Helper method to build the expected multipart form data part
def expected_form_part(boundary:, name:, value:, filename: nil)
lines = ["--#{boundary}"]
def expected_form_part(name:, value:, filename: nil)
lines = ["--#{test_boundary}"]
if filename
lines << "Content-Disposition: form-data; name=\"#{name}\"; filename=\"#{filename}\""
lines << 'Content-Type: application/octet-stream'
else
lines << "Content-Disposition: form-data; name=\"#{name}\""
end

lines << ''
lines << value
lines << "--#{boundary}"
lines << "--#{test_boundary}"
lines.join("\r\n")
end

describe 'uploading a build with valid parameters' do
it 'successfully uploads the build and returns the media details' do
with_tmp_file(named: test_filename, content: test_file_content) do |file_path|
# Stub the WordPress.com API request
stub_request(:post, "https://public-api.wordpress.com/rest/v1.1/sites/#{test_site_id}/media/new")
stub_request(:post, api_url)
.to_return(
status: 200,
body: {
media: [
{
ID: test_media_id,
URL: test_media_url,
date: test_date,
mime_type: test_mime_type,
file: test_filename,
post_ID: test_post_id
},
]
}.to_json,
body: stub_success_response,
headers: { 'Content-Type' => 'application/json' }
)

Expand Down Expand Up @@ -96,15 +102,13 @@ def expected_form_part(boundary:, name:, value:, filename: nil)

# Verify that the request was made with the correct parameters
expect(WebMock).to(
have_requested(:post, "https://public-api.wordpress.com/rest/v1.1/sites/#{test_site_id}/media/new").with do |req|
have_requested(:post, api_url).with do |req|
# Check that the request contains the expected headers
expect(req.headers['Content-Type']).to include('multipart/form-data')
expect(req.headers['Authorization']).to eq("Bearer #{test_api_token}")

boundary = req.headers['Content-Type'].match(/boundary=([^;]+)/)[1]

# Verify the media file is included with proper attributes
expect(req.body).to include(expected_form_part(boundary: boundary, name: 'media[]', value: test_file_content, filename: test_filename))
expect(req.body).to include(expected_form_part(name: 'media[]', value: test_file_content, filename: test_filename))

# Verify each parameter has the correct value
{
Expand All @@ -114,9 +118,10 @@ def expected_form_part(boundary:, name:, value:, filename: nil)
'platform' => test_platform,
'resource_type' => 'Build', # RESOURCE_TYPE constant
'version' => test_version,
'build_number' => test_build_number
'build_number' => test_build_number,
'install_type' => 'Full Install'
}.each do |name, value|
expect(req.body).to include(expected_form_part(boundary: boundary, name: name, value: value))
expect(req.body).to include(expected_form_part(name: name, value: value))
end

true
Expand All @@ -128,21 +133,10 @@ def expected_form_part(boundary:, name:, value:, filename: nil)
it 'successfully uploads the build with more optional parameters' do
with_tmp_file(named: test_filename, content: test_file_content) do |file_path|
# Stub the WordPress.com API request
stub_request(:post, "https://public-api.wordpress.com/rest/v1.1/sites/#{test_site_id}/media/new")
stub_request(:post, api_url)
.to_return(
status: 200,
body: {
media: [
{
ID: test_media_id,
URL: test_media_url,
date: test_date,
mime_type: test_mime_type,
file: test_filename,
post_ID: test_post_id
},
]
}.to_json,
body: stub_success_response,
headers: { 'Content-Type' => 'application/json' }
)

Expand All @@ -152,11 +146,13 @@ def expected_form_part(boundary:, name:, value:, filename: nil)
api_token: test_api_token,
product: test_product,
build_type: test_build_type,
install_type: 'Update',
visibility: :external,
platform: test_platform,
version: test_version,
build_number: test_build_number,
file_path: file_path,
sha: test_sha,
error_on_duplicate: true
)

Expand All @@ -170,11 +166,11 @@ def expected_form_part(boundary:, name:, value:, filename: nil)

# Verify that the request was made with the correct parameters
expect(WebMock).to(
have_requested(:post, "https://public-api.wordpress.com/rest/v1.1/sites/#{test_site_id}/media/new").with do |req|
boundary = req.headers['Content-Type'].match(/boundary=([^;]+)/)[1]

have_requested(:post, api_url).with do |req|
# Check that the visibility is set to External
expect(req.body).to include(expected_form_part(boundary: boundary, name: 'visibility', value: 'External'))
expect(req.body).to include(expected_form_part(name: 'visibility', value: 'External'))
expect(req.body).to include(expected_form_part(name: 'install_type', value: 'Update'))
expect(req.body).to include(expected_form_part(name: 'sha', value: test_sha))
true
end
)
Expand All @@ -184,7 +180,7 @@ def expected_form_part(boundary:, name:, value:, filename: nil)
it 'handles API validation errors properly' do
with_tmp_file(named: test_filename, content: test_file_content) do |file_path|
# Stub the WordPress.com API request to return a validation error
stub_request(:post, "https://public-api.wordpress.com/rest/v1.1/sites/#{test_site_id}/media/new")
stub_request(:post, api_url)
.to_return(
status: 400,
body: {
Expand Down Expand Up @@ -220,7 +216,7 @@ def expected_form_part(boundary:, name:, value:, filename: nil)
it 'handles non-JSON API errors properly' do
with_tmp_file(named: test_filename, content: test_file_content) do |file_path|
# Stub the WordPress.com API request to return a non-JSON error
stub_request(:post, "https://public-api.wordpress.com/rest/v1.1/sites/#{test_site_id}/media/new")
stub_request(:post, api_url)
.to_return(
status: 500,
body: 'Internal Server Error',
Expand All @@ -243,6 +239,53 @@ def expected_form_part(boundary:, name:, value:, filename: nil)
end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Upload to Apps CDN failed')
end
end

it 'does not include sha if it is not provided' do
with_tmp_file(named: test_filename, content: test_file_content) do |file_path|
stub_request(:post, api_url)
.to_return(
status: 200,
body: stub_success_response,
headers: { 'Content-Type' => 'application/json' }
)

run_described_fastlane_action(
site_id: test_site_id,
api_token: test_api_token,
product: test_product,
build_type: test_build_type,
visibility: test_visibility,
platform: test_platform,
version: test_version,
build_number: test_build_number,
file_path: file_path
)

expect(WebMock).to(
have_requested(:post, api_url).with do |req|
# Verify the media file is included with proper attributes
expect(req.body).to include(expected_form_part(name: 'media[]', value: test_file_content, filename: test_filename))

# Verify each parameter has the correct value
{
'product' => test_product,
'build_type' => test_build_type,
'visibility' => 'Internal', # Capitalized from :internal
'platform' => test_platform,
'resource_type' => 'Build', # RESOURCE_TYPE constant
'version' => test_version,
'build_number' => test_build_number,
'install_type' => 'Full Install'
}.each do |name, value|
expect(req.body).to include(expected_form_part(name: name, value: value))
end

expect(req.body).not_to match(/Content-Disposition: form-data; name="sha"/)
true
end
)
end
end
end

describe 'parameter validation' do
Expand Down Expand Up @@ -431,5 +474,23 @@ def expected_form_part(boundary:, name:, value:, filename: nil)
)
end.to raise_error(FastlaneCore::Interface::FastlaneError, "File not found at path 'non_existent_file.zip'")
end

it 'fails if install_type is not valid' do
with_tmp_file(named: test_filename) do |file_path|
expect do
run_described_fastlane_action(
site_id: test_site_id,
api_token: test_api_token,
product: test_product,
build_type: test_build_type,
visibility: test_visibility,
platform: test_platform,
version: test_version,
file_path: file_path,
install_type: 'InvalidType'
)
end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Install type must be one of: Full Install, Update')
end
end
end
end