From 766f65ed1979475176c08599c9ba70e19a05b7ff Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 2 Mar 2026 21:06:03 +0100 Subject: [PATCH 1/4] Add update_apps_cdn_build_metadata action to update visibility of existing CDN builds Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 +- .../common/update_apps_cdn_build_metadata.rb | 149 ++++++++++ spec/update_apps_cdn_build_metadata_spec.rb | 260 ++++++++++++++++++ 3 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb create mode 100644 spec/update_apps_cdn_build_metadata_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index e1c95a2e7..bef7a816d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ _None_ ### New Features -_None_ +- Added new `update_apps_cdn_build_metadata` action to update metadata (e.g. visibility) of an existing build on the Apps CDN without re-uploading the file. This enables a two-phase release flow: upload builds as Internal first, then flip to External at publish time. [#TBD] ### Bug Fixes diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb new file mode 100644 index 000000000..b4a253a99 --- /dev/null +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'fastlane/action' +require 'net/http' +require 'uri' +require 'json' + +module Fastlane + module Actions + class UpdateAppsCdnBuildMetadataAction < Action + VALID_VISIBILITIES = %i[internal external].freeze + VALID_POST_STATUS = %w[publish draft].freeze + + def self.run(params) + UI.message("Updating Apps CDN build metadata for post #{params[:post_id]}...") + + api_endpoint = "https://public-api.wordpress.com/rest/v1.1/sites/#{params[:site_id]}/posts/#{params[:post_id]}" + uri = URI.parse(api_endpoint) + + # Build the update form data + form_data = {} + form_data['terms[visibility]'] = params[:visibility].to_s.capitalize if params[:visibility] + form_data['status'] = params[:post_status] if params[:post_status] + + UI.user_error!('No metadata to update. Provide at least one of: visibility, post_status') if form_data.empty? + + # Create and send the HTTP request + request = Net::HTTP::Post.new(uri.request_uri) + request.body = URI.encode_www_form(form_data) + request['Content-Type'] = 'application/x-www-form-urlencoded' + request['Accept'] = 'application/json' + request['Authorization'] = "Bearer #{params[:api_token]}" + + response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http| + http.request(request) + end + + # Handle the response + case response + when Net::HTTPSuccess + result = JSON.parse(response.body) + post_id = result['ID'] + + UI.success("Successfully updated Apps CDN build metadata for post #{post_id}") + + { post_id: post_id } + else + UI.error("Failed to update Apps CDN build metadata: #{response.code} #{response.message}") + UI.error(response.body) + UI.user_error!('Update of Apps CDN build metadata failed') + end + end + + def self.description + 'Updates metadata of an existing build on the Apps CDN' + end + + def self.authors + ['Automattic'] + end + + def self.return_value + 'Returns a Hash containing { post_id: }. On error, raises a FastlaneError.' + end + + def self.details + <<~DETAILS + Updates metadata (such as visibility) for an existing build post on a WordPress blog + that has the Apps CDN plugin enabled, using the WordPress.com REST API. + See PCYsg-15tP-p2 internal a8c documentation for details about the Apps CDN plugin. + DETAILS + end + + def self.available_options + [ + FastlaneCore::ConfigItem.new( + key: :site_id, + env_name: 'APPS_CDN_SITE_ID', + description: 'The WordPress.com CDN site ID where the build was uploaded', + optional: false, + type: String, + verify_block: proc do |value| + UI.user_error!('Site ID cannot be empty') if value.to_s.empty? + end + ), + FastlaneCore::ConfigItem.new( + key: :post_id, + description: 'The ID of the build post to update', + optional: false, + type: Integer, + verify_block: proc do |value| + UI.user_error!('Post ID must be a positive integer') unless value.is_a?(Integer) && value.positive? + end + ), + FastlaneCore::ConfigItem.new( + key: :api_token, + env_name: 'WPCOM_API_TOKEN', + description: 'The WordPress.com API token for authentication', + optional: false, + type: String, + verify_block: proc do |value| + UI.user_error!('API token cannot be empty') if value.to_s.empty? + end + ), + FastlaneCore::ConfigItem.new( + key: :visibility, + description: 'The new visibility for the build (:internal or :external)', + optional: true, + type: Symbol, + verify_block: proc do |value| + UI.user_error!("Visibility must be one of: #{VALID_VISIBILITIES.map { "`:#{_1}`" }.join(', ')}") unless VALID_VISIBILITIES.include?(value.to_s.downcase.to_sym) + end + ), + FastlaneCore::ConfigItem.new( + key: :post_status, + description: "The new post status ('publish' or 'draft')", + optional: true, + type: String, + verify_block: proc do |value| + UI.user_error!("Post status must be one of: #{VALID_POST_STATUS.join(', ')}") unless VALID_POST_STATUS.include?(value) + end + ), + ] + end + + def self.is_supported?(platform) + true + end + + def self.example_code + [ + 'update_apps_cdn_build_metadata( + site_id: "12345678", + api_token: ENV["WPCOM_API_TOKEN"], + post_id: 98765, + visibility: :external + )', + 'update_apps_cdn_build_metadata( + site_id: "12345678", + api_token: ENV["WPCOM_API_TOKEN"], + post_id: 98765, + visibility: :internal, + post_status: "draft" + )', + ] + end + end + end +end diff --git a/spec/update_apps_cdn_build_metadata_spec.rb b/spec/update_apps_cdn_build_metadata_spec.rb new file mode 100644 index 000000000..1bfb6594f --- /dev/null +++ b/spec/update_apps_cdn_build_metadata_spec.rb @@ -0,0 +1,260 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' +require 'webmock/rspec' + +describe Fastlane::Actions::UpdateAppsCdnBuildMetadataAction do + let(:test_site_id) { '12345678' } + let(:test_post_id) { 98_765 } + let(:api_url) { "https://public-api.wordpress.com/rest/v1.1/sites/#{test_site_id}/posts/#{test_post_id}" } + let(:test_api_token) { 'test_api_token' } + + let(:stub_success_response) do + { + ID: test_post_id, + title: 'WordPress.com Studio 1.7.5', + status: 'publish', + terms: { + visibility: { + External: { ID: 1, name: 'External', slug: 'external' } + } + } + }.to_json + end + + before do + WebMock.disable_net_connect! + end + + after do + WebMock.allow_net_connect! + end + + describe 'updating visibility' do + it 'successfully updates the visibility to external' do + stub_request(:post, api_url) + .to_return( + status: 200, + body: stub_success_response, + headers: { 'Content-Type' => 'application/json' } + ) + + result = run_described_fastlane_action( + site_id: test_site_id, + api_token: test_api_token, + post_id: test_post_id, + visibility: :external + ) + + expect(result).to be_a(Hash) + expect(result[:post_id]).to eq(test_post_id) + + expect(WebMock).to( + have_requested(:post, api_url).with do |req| + expect(req.headers['Authorization']).to eq("Bearer #{test_api_token}") + expect(req.headers['Content-Type']).to eq('application/x-www-form-urlencoded') + expect(req.body).to include('terms%5Bvisibility%5D=External') + true + end + ) + end + + it 'successfully updates the visibility to internal' do + internal_response = { + ID: test_post_id, + terms: { + visibility: { + Internal: { ID: 2, name: 'Internal', slug: 'internal' } + } + } + }.to_json + + stub_request(:post, api_url) + .to_return( + status: 200, + body: internal_response, + headers: { 'Content-Type' => 'application/json' } + ) + + result = run_described_fastlane_action( + site_id: test_site_id, + api_token: test_api_token, + post_id: test_post_id, + visibility: :internal + ) + + expect(result[:post_id]).to eq(test_post_id) + + expect(WebMock).to( + have_requested(:post, api_url).with do |req| + expect(req.body).to include('terms%5Bvisibility%5D=Internal') + true + end + ) + end + end + + describe 'updating post_status' do + it 'successfully updates the post_status' do + stub_request(:post, api_url) + .to_return( + status: 200, + body: stub_success_response, + headers: { 'Content-Type' => 'application/json' } + ) + + result = run_described_fastlane_action( + site_id: test_site_id, + api_token: test_api_token, + post_id: test_post_id, + post_status: 'draft' + ) + + expect(result[:post_id]).to eq(test_post_id) + + expect(WebMock).to( + have_requested(:post, api_url).with do |req| + expect(req.body).to include('status=draft') + true + end + ) + end + end + + describe 'updating multiple fields' do + it 'successfully updates both visibility and post_status' do + stub_request(:post, api_url) + .to_return( + status: 200, + body: stub_success_response, + headers: { 'Content-Type' => 'application/json' } + ) + + result = run_described_fastlane_action( + site_id: test_site_id, + api_token: test_api_token, + post_id: test_post_id, + visibility: :external, + post_status: 'publish' + ) + + expect(result[:post_id]).to eq(test_post_id) + + expect(WebMock).to( + have_requested(:post, api_url).with do |req| + expect(req.body).to include('terms%5Bvisibility%5D=External') + expect(req.body).to include('status=publish') + true + end + ) + end + end + + describe 'error handling' do + it 'handles API errors properly' do + stub_request(:post, api_url) + .to_return( + status: 403, + body: { error: 'unauthorized', message: 'You are not authorized to access this resource.' }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect do + run_described_fastlane_action( + site_id: test_site_id, + api_token: test_api_token, + post_id: test_post_id, + visibility: :external + ) + end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Update of Apps CDN build metadata failed') + end + + it 'handles server errors properly' do + stub_request(:post, api_url) + .to_return( + status: 500, + body: 'Internal Server Error', + headers: { 'Content-Type' => 'text/plain' } + ) + + expect do + run_described_fastlane_action( + site_id: test_site_id, + api_token: test_api_token, + post_id: test_post_id, + visibility: :external + ) + end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Update of Apps CDN build metadata failed') + end + end + + describe 'parameter validation' do + it 'fails if site_id is empty' do + expect do + run_described_fastlane_action( + site_id: '', + api_token: test_api_token, + post_id: test_post_id, + visibility: :external + ) + end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Site ID cannot be empty') + end + + it 'fails if api_token is empty' do + expect do + run_described_fastlane_action( + site_id: test_site_id, + api_token: '', + post_id: test_post_id, + visibility: :external + ) + end.to raise_error(FastlaneCore::Interface::FastlaneError, 'API token cannot be empty') + end + + it 'fails if post_id is not a positive integer' do + expect do + run_described_fastlane_action( + site_id: test_site_id, + api_token: test_api_token, + post_id: -1, + visibility: :external + ) + end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Post ID must be a positive integer') + end + + it 'fails if visibility is not a valid symbol' do + expect do + run_described_fastlane_action( + site_id: test_site_id, + api_token: test_api_token, + post_id: test_post_id, + visibility: :public + ) + end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Visibility must be one of: `:internal`, `:external`') + end + + it 'fails if post_status is not a valid value' do + expect do + run_described_fastlane_action( + site_id: test_site_id, + api_token: test_api_token, + post_id: test_post_id, + post_status: 'invalid_status' + ) + end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Post status must be one of: publish, draft') + end + + it 'fails if no metadata to update is provided' do + stub_request(:post, api_url) # Shouldn't be reached + + expect do + run_described_fastlane_action( + site_id: test_site_id, + api_token: test_api_token, + post_id: test_post_id + ) + end.to raise_error(FastlaneCore::Interface::FastlaneError, 'No metadata to update. Provide at least one of: visibility, post_status') + end + end + +end From 345dad8560fb24c010f9766a19a974fd13efc21b Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Tue, 3 Mar 2026 11:33:39 +0100 Subject: [PATCH 2/4] Address PR review: add HTTP timeouts and fix changelog PR link Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 +- .../actions/common/update_apps_cdn_build_metadata.rb | 2 ++ spec/update_apps_cdn_build_metadata_spec.rb | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bef7a816d..8d6210303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ _None_ ### New Features -- Added new `update_apps_cdn_build_metadata` action to update metadata (e.g. visibility) of an existing build on the Apps CDN without re-uploading the file. This enables a two-phase release flow: upload builds as Internal first, then flip to External at publish time. [#TBD] +- Added new `update_apps_cdn_build_metadata` action to update metadata (e.g. visibility) of an existing build on the Apps CDN without re-uploading the file. This enables a two-phase release flow: upload builds as Internal first, then flip to External at publish time. [#701] ### Bug Fixes diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb index b4a253a99..66d4c337c 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb @@ -32,6 +32,8 @@ def self.run(params) request['Authorization'] = "Bearer #{params[:api_token]}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http| + http.open_timeout = 10 + http.read_timeout = 30 http.request(request) end diff --git a/spec/update_apps_cdn_build_metadata_spec.rb b/spec/update_apps_cdn_build_metadata_spec.rb index 1bfb6594f..bdabf2d75 100644 --- a/spec/update_apps_cdn_build_metadata_spec.rb +++ b/spec/update_apps_cdn_build_metadata_spec.rb @@ -256,5 +256,4 @@ end.to raise_error(FastlaneCore::Interface::FastlaneError, 'No metadata to update. Provide at least one of: visibility, post_status') end end - end From 4094e588cad73df3427e9eb506123920f5598332 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Tue, 3 Mar 2026 16:46:51 +0100 Subject: [PATCH 3/4] Remove WebMock.allow_net_connect! leak from spec after hook Co-Authored-By: Claude Opus 4.6 --- spec/update_apps_cdn_build_metadata_spec.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/update_apps_cdn_build_metadata_spec.rb b/spec/update_apps_cdn_build_metadata_spec.rb index bdabf2d75..1d61b226a 100644 --- a/spec/update_apps_cdn_build_metadata_spec.rb +++ b/spec/update_apps_cdn_build_metadata_spec.rb @@ -26,10 +26,6 @@ WebMock.disable_net_connect! end - after do - WebMock.allow_net_connect! - end - describe 'updating visibility' do it 'successfully updates the visibility to external' do stub_request(:post, api_url) From 36b03d8c4250848256eb1090166ffdf4b92e81d5 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Wed, 4 Mar 2026 13:52:56 +0100 Subject: [PATCH 4/4] Switch update_apps_cdn_build_metadata from v1.1 to WP REST API v2 The v1.1 API returns 500 for a8c_cdn_build custom post types. The WP REST API v2 endpoint works correctly for both reads and writes. - POST to /wp/v2/sites/{site_id}/a8c_cdn_build/{post_id} with JSON body - Visibility changes now look up taxonomy term IDs first - Response uses lowercase 'id' instead of 'ID' Co-Authored-By: Claude Opus 4.6 --- .../common/update_apps_cdn_build_metadata.rb | 56 +++++++--- spec/update_apps_cdn_build_metadata_spec.rb | 101 +++++++++++++----- 2 files changed, 115 insertions(+), 42 deletions(-) diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb index 66d4c337c..9af072850 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb @@ -14,20 +14,24 @@ class UpdateAppsCdnBuildMetadataAction < Action def self.run(params) UI.message("Updating Apps CDN build metadata for post #{params[:post_id]}...") - api_endpoint = "https://public-api.wordpress.com/rest/v1.1/sites/#{params[:site_id]}/posts/#{params[:post_id]}" - uri = URI.parse(api_endpoint) + # Build the JSON body for the WP REST API v2 + body = {} + body['status'] = params[:post_status] if params[:post_status] - # Build the update form data - form_data = {} - form_data['terms[visibility]'] = params[:visibility].to_s.capitalize if params[:visibility] - form_data['status'] = params[:post_status] if params[:post_status] + if params[:visibility] + term_id = lookup_visibility_term_id(site_id: params[:site_id], api_token: params[:api_token], visibility: params[:visibility]) + body['visibility'] = [term_id] + end - UI.user_error!('No metadata to update. Provide at least one of: visibility, post_status') if form_data.empty? + UI.user_error!('No metadata to update. Provide at least one of: visibility, post_status') if body.empty? + + api_endpoint = "https://public-api.wordpress.com/wp/v2/sites/#{params[:site_id]}/a8c_cdn_build/#{params[:post_id]}" + uri = URI.parse(api_endpoint) # Create and send the HTTP request request = Net::HTTP::Post.new(uri.request_uri) - request.body = URI.encode_www_form(form_data) - request['Content-Type'] = 'application/x-www-form-urlencoded' + request.body = JSON.generate(body) + request['Content-Type'] = 'application/json' request['Accept'] = 'application/json' request['Authorization'] = "Bearer #{params[:api_token]}" @@ -41,7 +45,7 @@ def self.run(params) case response when Net::HTTPSuccess result = JSON.parse(response.body) - post_id = result['ID'] + post_id = result['id'] UI.success("Successfully updated Apps CDN build metadata for post #{post_id}") @@ -53,6 +57,32 @@ def self.run(params) end end + # Look up the taxonomy term ID for a visibility value (e.g. :internal -> 1316) + def self.lookup_visibility_term_id(site_id:, api_token:, visibility:) + slug = visibility.to_s.downcase + api_endpoint = "https://public-api.wordpress.com/wp/v2/sites/#{site_id}/visibility?slug=#{slug}" + uri = URI.parse(api_endpoint) + + request = Net::HTTP::Get.new(uri.request_uri) + request['Accept'] = 'application/json' + request['Authorization'] = "Bearer #{api_token}" + + response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http| + http.open_timeout = 10 + http.read_timeout = 30 + http.request(request) + end + + case response + when Net::HTTPSuccess + terms = JSON.parse(response.body) + UI.user_error!("No visibility term found for '#{slug}'") if terms.empty? + terms.first['id'] + else + UI.user_error!("Failed to look up visibility term '#{slug}': #{response.code} #{response.message}") + end + end + def self.description 'Updates metadata of an existing build on the Apps CDN' end @@ -67,8 +97,8 @@ def self.return_value def self.details <<~DETAILS - Updates metadata (such as visibility) for an existing build post on a WordPress blog - that has the Apps CDN plugin enabled, using the WordPress.com REST API. + Updates metadata (such as post status or visibility) for an existing build post on a WordPress blog + that has the Apps CDN plugin enabled, using the WordPress.com REST API (WP v2). See PCYsg-15tP-p2 internal a8c documentation for details about the Apps CDN plugin. DETAILS end @@ -135,7 +165,7 @@ def self.example_code site_id: "12345678", api_token: ENV["WPCOM_API_TOKEN"], post_id: 98765, - visibility: :external + post_status: "publish" )', 'update_apps_cdn_build_metadata( site_id: "12345678", diff --git a/spec/update_apps_cdn_build_metadata_spec.rb b/spec/update_apps_cdn_build_metadata_spec.rb index 1d61b226a..8a8db2554 100644 --- a/spec/update_apps_cdn_build_metadata_spec.rb +++ b/spec/update_apps_cdn_build_metadata_spec.rb @@ -6,19 +6,20 @@ describe Fastlane::Actions::UpdateAppsCdnBuildMetadataAction do let(:test_site_id) { '12345678' } let(:test_post_id) { 98_765 } - let(:api_url) { "https://public-api.wordpress.com/rest/v1.1/sites/#{test_site_id}/posts/#{test_post_id}" } + let(:api_url) { "https://public-api.wordpress.com/wp/v2/sites/#{test_site_id}/a8c_cdn_build/#{test_post_id}" } + let(:visibility_term_url) { "https://public-api.wordpress.com/wp/v2/sites/#{test_site_id}/visibility" } let(:test_api_token) { 'test_api_token' } + let(:external_term_id) { 21_293 } + let(:internal_term_id) { 1_316 } + let(:stub_success_response) do { - ID: test_post_id, - title: 'WordPress.com Studio 1.7.5', + id: test_post_id, + title: { rendered: 'WordPress.com Studio 1.7.5' }, status: 'publish', - terms: { - visibility: { - External: { ID: 1, name: 'External', slug: 'external' } - } - } + visibility: [external_term_id], + class_list: ['visibility-external'] }.to_json end @@ -28,6 +29,14 @@ describe 'updating visibility' do it 'successfully updates the visibility to external' do + stub_request(:get, visibility_term_url) + .with(query: { 'slug' => 'external' }) + .to_return( + status: 200, + body: [{ 'id' => external_term_id, 'name' => 'External', 'slug' => 'external' }].to_json, + headers: { 'Content-Type' => 'application/json' } + ) + stub_request(:post, api_url) .to_return( status: 200, @@ -48,21 +57,27 @@ expect(WebMock).to( have_requested(:post, api_url).with do |req| expect(req.headers['Authorization']).to eq("Bearer #{test_api_token}") - expect(req.headers['Content-Type']).to eq('application/x-www-form-urlencoded') - expect(req.body).to include('terms%5Bvisibility%5D=External') + expect(req.headers['Content-Type']).to eq('application/json') + body = JSON.parse(req.body) + expect(body['visibility']).to eq([external_term_id]) true end ) end it 'successfully updates the visibility to internal' do + stub_request(:get, visibility_term_url) + .with(query: { 'slug' => 'internal' }) + .to_return( + status: 200, + body: [{ 'id' => internal_term_id, 'name' => 'Internal', 'slug' => 'internal' }].to_json, + headers: { 'Content-Type' => 'application/json' } + ) + internal_response = { - ID: test_post_id, - terms: { - visibility: { - Internal: { ID: 2, name: 'Internal', slug: 'internal' } - } - } + id: test_post_id, + visibility: [internal_term_id], + class_list: ['visibility-internal'] }.to_json stub_request(:post, api_url) @@ -83,7 +98,8 @@ expect(WebMock).to( have_requested(:post, api_url).with do |req| - expect(req.body).to include('terms%5Bvisibility%5D=Internal') + body = JSON.parse(req.body) + expect(body['visibility']).to eq([internal_term_id]) true end ) @@ -95,7 +111,7 @@ stub_request(:post, api_url) .to_return( status: 200, - body: stub_success_response, + body: { id: test_post_id, status: 'draft' }.to_json, headers: { 'Content-Type' => 'application/json' } ) @@ -110,7 +126,8 @@ expect(WebMock).to( have_requested(:post, api_url).with do |req| - expect(req.body).to include('status=draft') + body = JSON.parse(req.body) + expect(body['status']).to eq('draft') true end ) @@ -119,6 +136,14 @@ describe 'updating multiple fields' do it 'successfully updates both visibility and post_status' do + stub_request(:get, visibility_term_url) + .with(query: { 'slug' => 'external' }) + .to_return( + status: 200, + body: [{ 'id' => external_term_id, 'name' => 'External', 'slug' => 'external' }].to_json, + headers: { 'Content-Type' => 'application/json' } + ) + stub_request(:post, api_url) .to_return( status: 200, @@ -138,8 +163,9 @@ expect(WebMock).to( have_requested(:post, api_url).with do |req| - expect(req.body).to include('terms%5Bvisibility%5D=External') - expect(req.body).to include('status=publish') + body = JSON.parse(req.body) + expect(body['visibility']).to eq([external_term_id]) + expect(body['status']).to eq('publish') true end ) @@ -151,7 +177,7 @@ stub_request(:post, api_url) .to_return( status: 403, - body: { error: 'unauthorized', message: 'You are not authorized to access this resource.' }.to_json, + body: { code: 'rest_forbidden', message: 'You are not authorized.' }.to_json, headers: { 'Content-Type' => 'application/json' } ) @@ -160,7 +186,7 @@ site_id: test_site_id, api_token: test_api_token, post_id: test_post_id, - visibility: :external + post_status: 'publish' ) end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Update of Apps CDN build metadata failed') end @@ -178,10 +204,29 @@ site_id: test_site_id, api_token: test_api_token, post_id: test_post_id, - visibility: :external + post_status: 'publish' ) end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Update of Apps CDN build metadata failed') end + + it 'handles visibility term lookup failure' do + stub_request(:get, visibility_term_url) + .with(query: { 'slug' => 'external' }) + .to_return( + status: 200, + body: [].to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect do + run_described_fastlane_action( + site_id: test_site_id, + api_token: test_api_token, + post_id: test_post_id, + visibility: :external + ) + end.to raise_error(FastlaneCore::Interface::FastlaneError, "No visibility term found for 'external'") + end end describe 'parameter validation' do @@ -191,7 +236,7 @@ site_id: '', api_token: test_api_token, post_id: test_post_id, - visibility: :external + post_status: 'publish' ) end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Site ID cannot be empty') end @@ -202,7 +247,7 @@ site_id: test_site_id, api_token: '', post_id: test_post_id, - visibility: :external + post_status: 'publish' ) end.to raise_error(FastlaneCore::Interface::FastlaneError, 'API token cannot be empty') end @@ -213,7 +258,7 @@ site_id: test_site_id, api_token: test_api_token, post_id: -1, - visibility: :external + post_status: 'publish' ) end.to raise_error(FastlaneCore::Interface::FastlaneError, 'Post ID must be a positive integer') end @@ -241,8 +286,6 @@ end it 'fails if no metadata to update is provided' do - stub_request(:post, api_url) # Shouldn't be reached - expect do run_described_fastlane_action( site_id: test_site_id,