├── .yardopts ├── .hound.yml ├── circle.yml ├── lib ├── spaceship │ ├── version.rb │ ├── portal │ │ ├── portal.rb │ │ ├── portal_base.rb │ │ ├── app_group.rb │ │ ├── ui │ │ │ └── select_team.rb │ │ └── spaceship.rb │ ├── tunes │ │ ├── device_type.rb │ │ ├── user_detail.rb │ │ ├── tunes_base.rb │ │ ├── app_screenshot.rb │ │ ├── app_version_ref.rb │ │ ├── transit_app_file.rb │ │ ├── processing_build.rb │ │ ├── tunes.rb │ │ ├── spaceship.rb │ │ ├── app_image.rb │ │ ├── language_item.rb │ │ ├── app_trailer.rb │ │ ├── language_converter.rb │ │ ├── app_status.rb │ │ ├── app_details.rb │ │ └── build_train.rb │ ├── helper │ │ ├── plist_middleware.rb │ │ └── net_http_generic_request.rb │ ├── ui.rb │ ├── du │ │ ├── upload_file.rb │ │ └── utilities.rb │ └── launcher.rb ├── spaceship.rb └── assets │ └── languageMappingReadable.json ├── spec ├── tunes │ ├── fixtures │ │ ├── login_cntrl.js │ │ ├── landing_page.html │ │ ├── app_submission │ │ │ ├── complete_failed.json │ │ │ ├── complete_success.json │ │ │ └── start_success.json │ │ ├── app_version_ref.json │ │ ├── invalid_login.html │ │ ├── login_cookie_spam.txt │ │ ├── app_resolution_center_valid.json │ │ ├── testflight_submission_submit.json │ │ ├── user_detail.json │ │ ├── testers │ │ │ ├── existing_internal_testers.json │ │ │ ├── get_external.json │ │ │ └── get_internal.json │ │ ├── create_application_prefill_request.json │ │ ├── create_application_prefill_first_request.json │ │ ├── create_application_success.json │ │ ├── create_application_broken.json │ │ ├── create_application_wildcard_broken.json │ │ ├── create_application_first_broken.json │ │ ├── app_resolution_center.json │ │ ├── app_overview.json │ │ ├── app_details.json │ │ └── testflight_submission_start.json │ ├── login_spec.rb │ ├── language_item_spec.rb │ ├── app_details_spec.rb │ ├── tunes_client_spec.rb │ ├── language_converter_spec.rb │ ├── app_submission_spec.rb │ ├── build_train_spec.rb │ ├── testers_spec.rb │ └── build_spec.rb ├── portal │ ├── fixtures │ │ ├── aps_development.cer │ │ ├── profileContentDownload.action │ │ ├── create_profile_name_taken.txt │ │ ├── download_certificate_failure.html │ │ ├── deleteAppId.action.json │ │ ├── deleteApplicationGroup.action.json │ │ ├── deleteProvisioningProfile.action.json │ │ ├── listDevicesPage2-2.action.json │ │ ├── addApplicationGroup.action.json │ │ ├── listDevicesiPod.action.json │ │ ├── listDevicesTV.action.json │ │ ├── listDevicesWatch.action.json │ │ ├── certificateSigningRequest.certSigningRequest │ │ ├── listApplicationGroups.action.json │ │ ├── addDeviceResponse.action.plist │ │ ├── enterprise │ │ │ └── listCertRequests.action.json │ │ ├── listDevicesiPhone.action.json │ │ ├── certificateCreate.certRequest.json │ │ ├── downloaded_provisioning_profile.mobileprovision │ │ ├── addAppId.action.wildcard.json │ │ ├── list_certificates_filtered.json │ │ ├── addAppId.action.explicit.json │ │ ├── listDevices.action.json │ │ ├── getAppIdDetail.action.json │ │ ├── listTeams.action.json │ │ ├── listDevicesPage1-2.action.json │ │ ├── revokeCertificate.action.json │ │ ├── submitCertificateRequest.action.json │ │ ├── getProvisioningProfile.action.json │ │ ├── create_profile_success.json │ │ ├── repair_profile_success.json │ │ ├── listTeams_multiple.action.json │ │ ├── listCertRequests.action.json │ │ └── listApps.action.json │ ├── enterprise_spec.rb │ ├── app_group_spec.rb │ ├── app_spec.rb │ └── device_spec.rb ├── du │ ├── fixtures │ │ ├── upload_geojson_response_success.json │ │ ├── upload_valid.geojson │ │ ├── upload_invalid.GeoJSON │ │ ├── upload_image_success.json │ │ ├── upload_screenshot_response_success.json │ │ ├── upload_image_failed.json │ │ ├── upload_geojson_response_failed.json │ │ ├── upload_trailer_preview_response_success.json │ │ ├── upload_trailer_preview_2_response_success.json │ │ └── upload_trailer_response_success.json │ └── du_client_spec.rb ├── base_spec.rb ├── spec_helper.rb ├── spaceship_spec.rb ├── launcher_spec.rb ├── UI │ └── select_team_spec.rb └── spaceship_base_spec.rb ├── tasks └── rspec.rake ├── assets ├── fastlane.png ├── spaceship.png ├── spaceshipWhite.png ├── docs │ ├── AppVersions.png │ ├── BuildTrains.png │ └── Playground.png └── SpaceshipRecording.gif ├── .rspec ├── fastlane └── Fastfile ├── .travis.yml ├── Gemfile ├── Rakefile ├── .rubocop.yml ├── .gitignore ├── LICENSE ├── CONTRIBUTING.md ├── bin └── spaceship ├── spaceship.gemspec └── .rubocop_general.yml /.yardopts: -------------------------------------------------------------------------------- 1 | yardoc --no-private lib/**/*.rb -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | ruby: 2 | config_file: .rubocop.yml 3 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | ruby: 3 | version: 2.2.0 4 | -------------------------------------------------------------------------------- /lib/spaceship/version.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | VERSION = "0.19.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/login_cntrl.js: -------------------------------------------------------------------------------- 1 | 🤖 2 | itcServiceKey = '1234567890' 3 | 👽 4 | -------------------------------------------------------------------------------- /tasks/rspec.rake: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | RSpec::Core::RakeTask.new(:spec) 4 | -------------------------------------------------------------------------------- /assets/fastlane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corymsmith/spaceship/master/assets/fastlane.png -------------------------------------------------------------------------------- /assets/spaceship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corymsmith/spaceship/master/assets/spaceship.png -------------------------------------------------------------------------------- /assets/spaceshipWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corymsmith/spaceship/master/assets/spaceshipWhite.png -------------------------------------------------------------------------------- /assets/docs/AppVersions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corymsmith/spaceship/master/assets/docs/AppVersions.png -------------------------------------------------------------------------------- /assets/docs/BuildTrains.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corymsmith/spaceship/master/assets/docs/BuildTrains.png -------------------------------------------------------------------------------- /assets/docs/Playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corymsmith/spaceship/master/assets/docs/Playground.png -------------------------------------------------------------------------------- /assets/SpaceshipRecording.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corymsmith/spaceship/master/assets/SpaceshipRecording.gif -------------------------------------------------------------------------------- /spec/portal/fixtures/aps_development.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corymsmith/spaceship/master/spec/portal/fixtures/aps_development.cer -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | RSpec::Core::RakeTask.new(:spec) 4 | 5 | --require spec_helper 6 | --color 7 | --format d 8 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # Fetch and use the latest Fastfile from the fastlane main repository 2 | import_from_git(url: "https://github.com/KrauseFx/fastlane") 3 | -------------------------------------------------------------------------------- /spec/portal/fixtures/profileContentDownload.action: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corymsmith/spaceship/master/spec/portal/fixtures/profileContentDownload.action -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | before_install: 3 | - gem update --system 4 | - gem install bundler 5 | rvm: 6 | - 2.0.0 7 | - 2.1.6 8 | - 2.2.2 9 | script: bundle exec fastlane test 10 | -------------------------------------------------------------------------------- /spec/du/fixtures/upload_geojson_response_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "token" : "Purple1/v4/45/50/9d/45509d39-6a5d-7f55-f919-0fbc7436be61/pr_source.geojson", 3 | "type" : "SMGameCenterAvatarImageType.SOURCE", 4 | "dsId" : 1206675732 5 | } -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in .gemspec 4 | gemspec 5 | 6 | if `cd ..; git remote -v`.include?('countdown') 7 | gem 'credentials_manager', path: '../credentials_manager' 8 | end 9 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/landing_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | -------------------------------------------------------------------------------- /spec/portal/fixtures/create_profile_name_taken.txt: -------------------------------------------------------------------------------- 1 | Multiple profiles found with the name 'Test Name 3'. Please remove the duplicate profiles and try again.\nThere are no current certificates on this team matching the provided certificate IDs. -------------------------------------------------------------------------------- /spec/tunes/fixtures/app_submission/complete_failed.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": null, 3 | "messages": { 4 | "error": ["Problem processing review submission."], 5 | "info": null, 6 | "warn": null 7 | }, 8 | "statusCode": "SUCCESS" 9 | } 10 | -------------------------------------------------------------------------------- /spec/du/fixtures/upload_valid.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "MultiPolygon", 3 | "coordinates": [ 4 | [[[-122.7, 37.3], [-121.9, 37.3], [-121.9, 37.9], [-122.7, 37.9], [-122.7, 37.3]]], 5 | [[[-87.9, 41.5], [-87.3, 41.5], [-87.3, 42.1], [-87.9, 42.1], [-87.9, 41.5]]] 6 | ] 7 | } -------------------------------------------------------------------------------- /spec/du/fixtures/upload_invalid.GeoJSON: -------------------------------------------------------------------------------- 1 | {"type":"MultiPolygon","coordinates": 2 | [[45.56497960813695, -74.22161340332036],[45.823011917542566, -73.46767663574224],[45.47356846799377, -73.30562829589849],[45.34051720106048, -73.51299523925786],[45.56497960813695, -74.22161340332036]]} 3 | -------------------------------------------------------------------------------- /spec/tunes/login_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Tunes do 4 | describe ".login" do 5 | it "works with valid data" do 6 | client = Spaceship::Tunes.login 7 | expect(client).to be_instance_of(Spaceship::TunesClient) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/app_version_ref.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "ssoTokenForVideo": "the_sso_token_for_video", 4 | "ssoTokenForImage": "the_sso_token_for_image" 5 | }, 6 | "messages": { 7 | "warn": null, 8 | "error": null, 9 | "info": null 10 | }, 11 | "statusCode": "SUCCESS" 12 | } -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rubocop/rake_task" 3 | 4 | Dir.glob('tasks/**/*.rake').each(&method(:import)) 5 | 6 | task default: :spec 7 | 8 | task :test do 9 | sh "../fastlane/bin/fastlane test" 10 | end 11 | 12 | task :push do 13 | sh "../fastlane/bin/fastlane release" 14 | end 15 | -------------------------------------------------------------------------------- /lib/spaceship/portal/portal.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship/portal/portal_base' 2 | require 'spaceship/portal/app' 3 | require 'spaceship/portal/app_group' 4 | require 'spaceship/portal/app_service' 5 | require 'spaceship/portal/certificate' 6 | require 'spaceship/portal/device' 7 | require 'spaceship/portal/provisioning_profile' 8 | require 'spaceship/portal/portal_client' 9 | -------------------------------------------------------------------------------- /lib/spaceship/portal/portal_base.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | class PortalBase < Spaceship::Base 3 | class << self 4 | def client 5 | ( 6 | @client or 7 | Spaceship::Portal.client or 8 | raise "Please login using `Spaceship::Portal.login('user', 'password')`" 9 | ) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/du/fixtures/upload_image_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "Purple7/v4/65/04/4d/65044dae-15b0-a5e0-d021-5aa4162a03a3/pr_source.jpg", 3 | "tokenType": "AssetToken", 4 | "type": "MZLargeApplicationIconType.SOURCE", 5 | "width": 1024, 6 | "height": 1024, 7 | "hasAlpha": false, 8 | "dsId": 1206675732, 9 | "length": 198508, 10 | "md5": "d41d8cd98f00b204e9800998ecf8427e" 11 | } 12 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | 2 | ################## 3 | # All rules specific to this repo 4 | ################## 5 | 6 | inherit_from: .rubocop_general.yml 7 | 8 | 9 | ################## 10 | # TODO 11 | ################## 12 | 13 | # Offense count: 19 14 | Style/Documentation: 15 | Enabled: false 16 | 17 | # Offense count: 2 18 | # Cop supports --auto-correct. 19 | Style/PerlBackrefs: 20 | Enabled: false 21 | 22 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/device_type.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | class DeviceType 4 | @types = ['iphone4', 'iphone35', 'iphone6', 'iphone6Plus', 'ipad', 'ipadPro', 'watch', 'appleTV', 'desktop'] 5 | class << self 6 | attr_accessor :types 7 | 8 | def exists?(type) 9 | types.include? type 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/user_detail.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | class UserDetail < TunesBase 4 | attr_accessor :content_provider_id 5 | 6 | attr_mapping( 7 | 'contentProviderId' => :content_provider_id 8 | ) 9 | 10 | class << self 11 | def factory(attrs) 12 | self.new(attrs) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/tunes_base.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | class TunesBase < Spaceship::Base 4 | class << self 5 | def client 6 | ( 7 | @client or 8 | Spaceship::Tunes.client or 9 | raise "Please login using `Spaceship::Tunes.login('user', 'password')`" 10 | ) 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/du/fixtures/upload_screenshot_response_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "token" : "Purple69/v4/66/c9/26/66c9269c-779e-1d75-431b-a34c69fe86ad/pr_source.png", 3 | "tokenType" : "AssetToken", 4 | "type" : "MZSortedN41ScreenShotImageType.SOURCE", 5 | "width" : 1136, 6 | "height" : 640, 7 | "hasAlpha" : false, 8 | "dsId" : 1206675732, 9 | "length" : 317323, 10 | "md5" : "d41d8cd98f00b204e9800998ecf8427e" 11 | } -------------------------------------------------------------------------------- /spec/du/fixtures/upload_image_failed.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode" : 400, 3 | "errorCodes" : [ "IMG_ALPHA_NOT_ALLOWED" ], 4 | "suggestionCode" : "IMG_RESAVE_NO_ALPHA", 5 | "nonLocalizedMessage" : "Alpha is not allowed. Please edit the image to remove alpha and re-save it.", 6 | "localizedMessage" : "Alpha is not allowed. Please edit the image to remove alpha and re-save it.", 7 | "width" : 1024, 8 | "height" : 1024 9 | } 10 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/invalid_login.html: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | Your Apple ID or password was entered incorrectly -------------------------------------------------------------------------------- /spec/portal/fixtures/download_certificate_failure.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Try your request again. 5 | 6 | 7 |

We are unable to process your request.

8 |

An unknown error occurred. Please try again. 9 |

12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/spaceship/helper/plist_middleware.rb: -------------------------------------------------------------------------------- 1 | require 'faraday_middleware/response_middleware' 2 | 3 | module FaradayMiddleware 4 | class PlistMiddleware < ResponseMiddleware 5 | dependency do 6 | require 'plist' unless defined?(::Plist) 7 | end 8 | 9 | define_parser do |body| 10 | Plist.parse_xml(body) 11 | end 12 | end 13 | end 14 | 15 | Faraday::Response.register_middleware(plist: FaradayMiddleware::PlistMiddleware) 16 | -------------------------------------------------------------------------------- /spec/tunes/language_item_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Tunes::LanguageItem do 4 | before do 5 | Spaceship::Tunes.login 6 | end 7 | 8 | describe "#inspect" do 9 | it "prints out all languages with their values" do 10 | str = Spaceship::Application.all.first.edit_version.description.inspect 11 | expect(str).to eq("German: My title\nEnglish: Super Description here\n") 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/app_screenshot.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | # Represents a screenshot hosted on iTunes Connect 4 | class AppScreenshot < Spaceship::Tunes::AppImage 5 | attr_accessor :device_type 6 | 7 | attr_accessor :language 8 | 9 | class << self 10 | # Create a new object based on a hash. 11 | def factory(attrs) 12 | self.new(attrs) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/du/fixtures/upload_geojson_response_failed.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode" : 400, 3 | "errorCodes" : [ "FILE_GEOJSON_FIELD_NOT_SUPPORTED" ], 4 | "suggestionCode" : "TRANSIT_APP_FILE_INVALID_JSON", 5 | "nonLocalizedMessage" : "The routing app coverage file is incorrectly formatted. Please refer to Apple's documentation.", 6 | "localizedMessage" : "The routing app coverage file is in the wrong format. For more information, see the Location and Maps Programming Guide in the iOS Developer Library." 7 | } -------------------------------------------------------------------------------- /lib/spaceship/helper/net_http_generic_request.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | 3 | ## monkey-patch Net::HTTP 4 | # 5 | # Certain apple endpoints return 415 responses if a Content-Type is supplied. 6 | # Net::HTTP will default a content-type if none is provided by faraday 7 | # This monkey-patch allows us to leave out the content-type if we do not specify one. 8 | module Net 9 | class HTTPGenericRequest 10 | def supply_default_content_type 11 | return if content_type 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/app_version_ref.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | class AppVersionRef < TunesBase 4 | attr_accessor :sso_token_for_image 5 | attr_accessor :sso_token_for_video 6 | 7 | attr_mapping( 8 | 'ssoTokenForImage' => :sso_token_for_image, 9 | 'ssoTokenForVideo' => :sso_token_for_video 10 | ) 11 | 12 | class << self 13 | def factory(attrs) 14 | self.new(attrs) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/login_cookie_spam.txt: -------------------------------------------------------------------------------- 1 | NSC_17ofu-jud-jud-mc=ffffffff12920b0045525d5f4f58455e445a4a426d73; s_cc=true; s_vi=[CS]v1|2AC9516485013F89-6000010BC0496D7C[CE]; s_sq=applesuperglobal%3D%2526pid%253DiTC%252520Sign%252520In%2aa520Redesign%2526pidt%253D1%2526oid%253Dhttps%25253A%25252F%25252Fitunesconnect.apple.com%25252Fitc%25252Fimages%25252Ftransparent.gif%2526ot%253DIMAGE; ds01=A2977FA07F75801ED12F0F3C00A4DA168BC9D5EC52B56806042E6F24F073930DF6D64A934B47B42BE51D346953899F58B0AFB0803BCD287571400E548000BD63; myacinfo=DAWTKN; wosid=xBJMOVttbAQ1Cwlt8ktafw; woinst=3363; itctx=abc:def; -------------------------------------------------------------------------------- /spec/tunes/fixtures/app_resolution_center_valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "replyConstraints": { 7 | "minLength": 1, 8 | "maxLength": 4000 9 | }, 10 | "appNotes": { 11 | "threads": [] 12 | }, 13 | "betaNotes": { 14 | "threads": [] 15 | }, 16 | "appMessages": { 17 | "threads": [] 18 | } 19 | }, 20 | "messages": { 21 | "warn": null, 22 | "error": null, 23 | "info": null 24 | }, 25 | "statusCode": "SUCCESS" 26 | } -------------------------------------------------------------------------------- /spec/portal/fixtures/deleteAppId.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "responseId": "20399f66-db67-49dd-b5c0-9db7ef5ca442", 3 | "resultCode": 0, 4 | "creationTimestamp": "2015-05-25T14:44:24Z", 5 | "protocolVersion": "QH65B2", 6 | "requestId": "dc4a31db-c440-4737-ad6f-1416ec67b050", 7 | "userLocale": "en_US", 8 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/profile/deleteAppId.action", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "httpResponseHeaders": null 16 | } 17 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/transit_app_file.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | # Represents a geo json 4 | class TransitAppFile < TunesBase 5 | attr_accessor :asset_token 6 | 7 | attr_accessor :name 8 | 9 | attr_accessor :time_stamp 10 | 11 | attr_accessor :url 12 | 13 | attr_mapping( 14 | 'assetToken' => :asset_token, 15 | 'timeStemp' => :time_stamp, 16 | 'url' => :url, 17 | 'name' => :name 18 | ) 19 | 20 | class << self 21 | def factory(attrs) 22 | self.new(attrs) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/portal/fixtures/deleteApplicationGroup.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "responseId": "20399f66-db67-49dd-b5c0-9db7ef5ca442", 3 | "resultCode": 0, 4 | "creationTimestamp": "2015-05-25T14:44:24Z", 5 | "protocolVersion": "QH65B2", 6 | "requestId": "dc4a31db-c440-4737-ad6f-1416ec67b050", 7 | "userLocale": "en_US", 8 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/profile/deleteApplicationGroup.action", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "httpResponseHeaders": null 16 | } 17 | -------------------------------------------------------------------------------- /spec/portal/fixtures/deleteProvisioningProfile.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "responseId": "20399f66-db67-49dd-b5c0-9db7ef5ca441", 3 | "resultCode": 0, 4 | "creationTimestamp": "2015-05-25T14:44:24Z", 5 | "protocolVersion": "QH65B2", 6 | "requestId": "dc4a31db-c440-4737-ad6f-1416ec67b059", 7 | "userLocale": "en_US", 8 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/profile/deleteProvisioningProfile.action", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "httpResponseHeaders": null 16 | } 17 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/testflight_submission_submit.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "usesEncryption": null, 7 | "encryptionUpdated": null, 8 | "isExempt": null, 9 | "containsProprietaryCryptography": null, 10 | "containsThirdPartyCryptography": null, 11 | "availableOnFrenchStore": null, 12 | "ccatFile": null, 13 | "appType": "iOS App", 14 | "platform": "ios", 15 | "exportComplianceRequired": false 16 | }, 17 | "messages": { 18 | "warn": null, 19 | "error": null, 20 | "info": ["Success"] 21 | }, 22 | "statusCode": "SUCCESS" 23 | } -------------------------------------------------------------------------------- /spec/tunes/fixtures/user_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "isLocaleNameReversed": false, 4 | "isEmailInvalid": false, 5 | "hasContractInfo": false, 6 | "canEditITCUsersAndRoles": true, 7 | "canViewITCUsersAndRoles": true, 8 | "canEditIAPUsersAndRoles": true, 9 | "contentProviderType": "Purple Software", 10 | "displayName": "Foo Bar", 11 | "userFeatures": [], 12 | "contentProviderId": "1234567", 13 | "visibility": true, 14 | "DYCVisibility": false, 15 | "contentProvider": "We Want To Believe", 16 | "userName": "foo@bar.com" 17 | }, 18 | "messages": { 19 | "warn": null, 20 | "error": null, 21 | "info": null 22 | }, 23 | "statusCode": "SUCCESS" 24 | } -------------------------------------------------------------------------------- /spec/du/fixtures/upload_trailer_preview_response_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "token" : "Purple69/v4/5f/2b/81/5f2b814d-1083-5509-61fb-c0845f7a9374/pr_source.jpg", 3 | "tokenType" : "AssetToken", 4 | "type" : "MZSortedTabletScreenShotImageType.SOURCE", 5 | "width" : 768, 6 | "height" : 1024, 7 | "hasAlpha" : false, 8 | "dsId" : 1206675732, 9 | "descriptionDoc" : "10247688RGBfalsefalse", 10 | "length" : 34803, 11 | "md5" : "d41d8cd98f00b204e9800998ecf8427e" 12 | } -------------------------------------------------------------------------------- /spec/du/fixtures/upload_trailer_preview_2_response_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "token" : "Purple70/v4/5f/2b/81/5f2b814d-1083-5509-61fb-c0845f7a9374/pr_source.jpg", 3 | "tokenType" : "AssetToken", 4 | "type" : "MZSortedTabletScreenShotImageType.SOURCE", 5 | "width" : 768, 6 | "height" : 1024, 7 | "hasAlpha" : false, 8 | "dsId" : 1206675732, 9 | "descriptionDoc" : "10247688RGBfalsefalse", 10 | "length" : 34803, 11 | "md5" : "d41d8cd98f00b204e9800998ecf8427e" 12 | } -------------------------------------------------------------------------------- /spec/portal/fixtures/listDevicesPage2-2.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-05-24T16:37:33Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/device/listDevices.action", 8 | "responseId": "558ba377-d979-45c6-b2f8-ee6d2d692aaa", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": true, 12 | "pageNumber": 2, 13 | "pageSize": 8, 14 | "totalRecords": 9, 15 | "httpResponseHeaders": null, 16 | "devices": [{ 17 | "deviceId": "XJXGVS4AAAA", 18 | "name": "The last phone", 19 | "deviceNumber": "4c24a7ee5c8674847f49b4fb2d87483053faaaaa", 20 | "devicePlatform": "ios", 21 | "status": "c" 22 | }] 23 | } -------------------------------------------------------------------------------- /spec/portal/fixtures/addApplicationGroup.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-08-06T11:08:07Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/identifiers/addApplicationGroup.action", 8 | "responseId": "b9d17c2b-6acd-40b1-964b-882c01e05605", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "applicationGroup": { 16 | "name": "Production App Group", 17 | "prefix": "9J57U9392R", 18 | "identifier": "group.tools.fastlane", 19 | "status": "current", 20 | "applicationGroup": "NR5BH6N89Z" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/spaceship/ui.rb: -------------------------------------------------------------------------------- 1 | paths = Dir[File.expand_path "**/ui/*.rb", File.dirname(__FILE__)] 2 | raise "Could not find UI classes to import" unless paths.count > 0 3 | paths.each do |file| 4 | require file 5 | end 6 | 7 | module Spaceship 8 | class Client 9 | # Public getter for all UI related code 10 | # rubocop:disable Style/MethodName 11 | def UI 12 | UserInterface.new(self) 13 | end 14 | # rubocop:enable Style/MethodName 15 | 16 | # All User Interface related code lives in this class 17 | class UserInterface 18 | # Access the client this UserInterface object is for 19 | attr_reader :client 20 | 21 | # Is called by the client to generate one instance of UserInterface 22 | def initialize(c) 23 | @client = c 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/portal/fixtures/listDevicesiPod.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-25T00:02:42Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443/services-account/QH65B2/account/ios/device/listDevices.action", 8 | "responseId": "21bcfffa-90cb-4e2b-8143-a20dca8a60b5", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": 1, 13 | "pageSize": 100, 14 | "totalRecords": 1, 15 | "devices": [ 16 | { 17 | "deviceId": "CCCCCCCCCC", 18 | "name": "Personal iPhone", 19 | "deviceNumber": "97467684eb8dfa3c6d272eac3890dab0d001c706", 20 | "devicePlatform": "ios", 21 | "status": "c", 22 | "model": null, 23 | "deviceClass": "ipod" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /spec/portal/fixtures/listDevicesTV.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-25T00:02:42Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443/services-account/QH65B2/account/ios/device/listDevices.action", 8 | "responseId": "21bcfffa-90cb-4e2b-8143-a20dca8a60b5", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": 1, 13 | "pageSize": 100, 14 | "totalRecords": 1, 15 | "devices": [ 16 | { 17 | "deviceId": "EEEEEEEEEE", 18 | "name": "Tracy's Apple TV", 19 | "deviceNumber": "8defe35b2cad44affacabd124834acbd8746ff34", 20 | "devicePlatform": "ios", 21 | "status": "c", 22 | "model": "The new Apple TV", 23 | "deviceClass": "tvOS" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /spec/portal/fixtures/listDevicesWatch.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-25T00:02:42Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443/services-account/QH65B2/account/ios/device/listDevices.action", 8 | "responseId": "21bcfffa-90cb-4e2b-8143-a20dca8a60b5", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": 1, 13 | "pageSize": 100, 14 | "totalRecords": 1, 15 | "devices": [ 16 | { 17 | "deviceId": "FFFFFFFFFF", 18 | "name": "Tracy's Watch", 19 | "deviceNumber": "8defe35b2cad44aff7d8e9dfe4ca4d2fb94ae509", 20 | "devicePlatform": "ios", 21 | "status": "c", 22 | "model": "Apple Watch 38mm", 23 | "deviceClass": "watch" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /lib/spaceship.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship/version' 2 | require 'spaceship/base' 3 | require 'spaceship/client' 4 | require 'spaceship/launcher' 5 | 6 | # Dev Portal 7 | require 'spaceship/portal/portal' 8 | require 'spaceship/portal/spaceship' 9 | 10 | # iTunes Connect 11 | require 'spaceship/tunes/tunes' 12 | require 'spaceship/tunes/spaceship' 13 | 14 | # To support legacy code 15 | module Spaceship 16 | # Dev Portal 17 | Certificate = Spaceship::Portal::Certificate 18 | ProvisioningProfile = Spaceship::Portal::ProvisioningProfile 19 | Device = Spaceship::Portal::Device 20 | App = Spaceship::Portal::App 21 | AppGroup = Spaceship::Portal::AppGroup 22 | AppService = Spaceship::Portal::AppService 23 | 24 | # iTunes Connect 25 | AppVersion = Spaceship::Tunes::AppVersion 26 | AppSubmission = Spaceship::Tunes::AppSubmission 27 | Application = Spaceship::Tunes::Application 28 | end 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /lib/bundler/man/ 26 | 27 | # for a library or gem, you might want to ignore these files since the code is 28 | # intended to run in multiple environments; otherwise, check them in: 29 | # Gemfile.lock 30 | # .ruby-version 31 | # .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | 36 | # OS X Junk 37 | .DS_Store 38 | 39 | # Added by Felix 40 | /*.itmsp 41 | .idea 42 | spec/fixtures/*.itmsp/*.png 43 | /integration 44 | Gemfile.lock 45 | fastlane/report.xml 46 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/processing_build.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | # Represents a build which doesn't have a version number yet and is either processing or is stuck 4 | class ProcessingBuild < Build 5 | # @return [String] The state of this build 6 | # @example 7 | # ITC.apps.betaProcessingStatus.InvalidBinary 8 | # @example 9 | # ITC.apps.betaProcessingStatus.Created 10 | # @example 11 | # ITC.apps.betaProcessingStatus.Uploaded 12 | attr_accessor :state 13 | 14 | # @return (Integer) The number of ticks since 1970 (e.g. 1413966436000) 15 | attr_accessor :upload_date 16 | 17 | attr_mapping( 18 | 'processingState' => :state, 19 | 'uploadDate' => :upload_date 20 | ) 21 | 22 | class << self 23 | def factory(attrs) 24 | self.new(attrs) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/assets/languageMappingReadable.json: -------------------------------------------------------------------------------- 1 | { 2 | "Brazilian Portuguese": "Brazilian Portuguese", 3 | "Danish": "Danish", 4 | "Dutch": "Dutch", 5 | "English": "English", 6 | "English_Australian": "Australian English", 7 | "English_CA": "Canadian English", 8 | "English_UK": "UK English", 9 | "Finnish": "Finnish", 10 | "French": "French", 11 | "French_CA": "Canadian French", 12 | "German": "German", 13 | "Greek": "Greek", 14 | "Indonesian": "Indonesian", 15 | "Italian": "Italian", 16 | "Japanese": "Japanese", 17 | "Korean": "Korean", 18 | "Malay": "Malay", 19 | "Norwegian": "Norwegian", 20 | "Portuguese": "Portuguese", 21 | "Russian": "Russian", 22 | "Simplified Chinese": "Simplified Chinese", 23 | "Spanish": "Spanish", 24 | "Spanish_MX": "Mexican Spanish", 25 | "Swedish": "Swedish", 26 | "Thai": "Thai", 27 | "Traditional Chinese": "Traditional Chinese", 28 | "Turkish": "Turkish", 29 | "Vietnamese": "Vietnamese" 30 | } -------------------------------------------------------------------------------- /lib/spaceship/tunes/tunes.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship/tunes/tunes_base' 2 | require 'spaceship/tunes/application' 3 | require 'spaceship/tunes/app_version' 4 | require 'spaceship/tunes/app_submission' 5 | require 'spaceship/tunes/tunes_client' 6 | require 'spaceship/tunes/language_item' 7 | require 'spaceship/tunes/app_status' 8 | require 'spaceship/tunes/app_image' 9 | require 'spaceship/tunes/app_version_ref' 10 | require 'spaceship/tunes/transit_app_file' 11 | require 'spaceship/tunes/user_detail' 12 | require 'spaceship/tunes/app_screenshot' 13 | require 'spaceship/tunes/language_converter' 14 | require 'spaceship/tunes/build' 15 | require 'spaceship/tunes/processing_build' 16 | require 'spaceship/tunes/build_train' 17 | require 'spaceship/tunes/device_type' 18 | require 'spaceship/tunes/app_trailer' 19 | require 'spaceship/tunes/tester' 20 | require 'spaceship/tunes/app_details' 21 | 22 | # File Uploads 23 | require 'spaceship/du/utilities' 24 | require 'spaceship/du/upload_file' 25 | require 'spaceship/du/du_client' 26 | -------------------------------------------------------------------------------- /spec/portal/fixtures/certificateSigningRequest.certSigningRequest: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIClDCCAXwCAQAwTzEnMCUGCSqGSIb3DQEJARYYc3RlZmFuLm5hdGNoZXZAZ21h 3 | aWwuY29tMRcwFQYDVQQDDA5TdGVmYW4gTmF0Y2hldjELMAkGA1UEBhMCVVMwggEi 4 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCn7ehaeM1yXqGEbd+FlsqwZTbI 5 | 7SyG38tb5BLvjTIHppsCTVLvInJbW/GhyLxrI+mKeyk4zI4JKIKOkrmKRtF/bv6w 6 | LutQnJSTw09Qo/X/uqNtgxabCBMlTKpNTE49PGYri/mRnc+y6PzwA/t0xKNk1cBI 7 | YpQ4nxZzTNXAwiAy+fUzfby2tQsKrb19twM+7PdVeDv20MPAoyfusx5IKRIIftSJ 8 | +re8nCYM2vJGPbsF/nutDrdUOrJoNAn7ouXgn06dPe9P1EXvv7A59zaFRDwnIlVa 9 | eJnN80FB4BLPAGoDADPtt50iEWDNOCrGrz6TfNgsf+F9nwUn56FcQh0w+RHvAgMB 10 | AAGgADANBgkqhkiG9w0BAQUFAAOCAQEAHtvAId/YNGisy3ENM7klf7OeCgsYecqW 11 | XyqGgFEpLsf/S1mhmKNod62iDr2Cd2WcbHL/5exN7jXZ9yCg5qZqmY5M0P7Nv/wr 12 | jU3KQ/A5qE8FeLs5wlnFk4g3rOYvLc3ZM7gWIGnc1KMcER6LFN+W4va3bPeokKvR 13 | WO7G8+fkwpVP8nTE8/rQMJB2pTK50uFNVp94ASfD+JQLfsBfWHFeGX+T1FCjjF9c 14 | Xa27xMEh6nxxTqHvkbi9HE9OH4rugCIh46TXjNGAwYax1bnc/y8nWR+7Y8jDscm8 15 | ZRu+PuSTeHX5Z8bjddr4DwSbwFssgcL8/6xhSDudp9TXv8jrQui02Q== 16 | -----END CERTIFICATE REQUEST----- -------------------------------------------------------------------------------- /spec/portal/fixtures/listApplicationGroups.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-08-06T10:55:20Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": "7d14e816-0f2e-42a0-81d1-e4c151d0c369", 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/identifiers/listApplicationGroups.action", 8 | "responseId": "4bc3cae3-fad8-4096-b658-1878ba013b2b", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": true, 12 | "pageNumber": 1, 13 | "pageSize": 500, 14 | "totalRecords": 2, 15 | "httpResponseHeaders": null, 16 | "applicationGroupList": [{ 17 | "name": "First group", 18 | "prefix": "9J57U9392R", 19 | "identifier": "group.com.example.one", 20 | "status": "current", 21 | "applicationGroup": "44V62UZ8L7" 22 | }, { 23 | "name": "Group II", 24 | "prefix": "9J57U9392R", 25 | "identifier": "group.com.example.two", 26 | "status": "current", 27 | "applicationGroup": "2GKKV64NUG" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /spec/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Base do 4 | before { Spaceship.login } 5 | let(:client) { Spaceship::App.client } 6 | 7 | describe "#inspect" do 8 | it "contains the relevant data" do 9 | app = Spaceship::App.all.first 10 | output = app.inspect 11 | expect(output).to include "B7JBD8LHAA" 12 | expect(output).to include "The App Name" 13 | end 14 | 15 | it "prints out references" do 16 | Spaceship::Tunes.login 17 | app = Spaceship::Application.all.first 18 | v = app.live_version 19 | output = v.inspect 20 | expect(output).to include "Tunes::AppVersion" 21 | expect(output).to include "Tunes::Application" 22 | end 23 | end 24 | 25 | it "doesn't blow up if it was initialized with a nil data hash" do 26 | hash = Spaceship::Base::DataHash.new(nil) 27 | expect { hash["key"] }.not_to raise_exception 28 | end 29 | 30 | it "allows modification of values and properly retrieving them" do 31 | app = Spaceship::App.all.first 32 | app.name = "12" 33 | expect(app.name).to eq("12") 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/portal/enterprise_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::ProvisioningProfile do 4 | describe "Enterprise Profiles" do 5 | before do 6 | Spaceship.login 7 | adp_enterprise_stubbing 8 | end 9 | let(:client) { Spaceship::ProvisioningProfile.client } 10 | 11 | describe "List the code signing certificate as In House profiles" do 12 | it "uses the correct class" do 13 | certs = Spaceship::Certificate::InHouse.all 14 | expect(certs.count).to eq(1) 15 | 16 | cert = certs.first 17 | expect(cert).to be_kind_of(Spaceship::Certificate::InHouse) 18 | expect(cert.name).to eq("SunApps GmbH") 19 | end 20 | end 21 | 22 | describe "Create a new In House Profile" do 23 | it "uses the correct type for the create request" do 24 | cert = Spaceship::Certificate::InHouse.all.first 25 | result = Spaceship::ProvisioningProfile::InHouse.create!(name: 'Delete Me', bundle_id: 'net.sunapps.1', certificate: cert) 26 | expect(result.raw_data['provisioningProfileId']).to eq('W2MY88F6GE') 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Felix Krause 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `fastlane` and its tools 2 | 3 | To clone the [fastlane](https://fastlane.tools) repos, use the [countdown](https://github.com/fastlane/countdown) repo. It will help you set up the development environment within minutes. 4 | 5 | ## New Issues 6 | 7 | Before submitting a new issue, do the following: 8 | 9 | - Verify you're runing the latest version by running `spaceship -v` and compare it with the [project page on GitHub](https://github.com/fastlane/spaceship). 10 | - Verify you have Xcode tools installed by running `xcode-select --install`. 11 | - Make sure to read through the [README](https://github.com/KrauseFx/spaceship) of the project. 12 | 13 | 14 | When submitting a new issue, please provide the following information: 15 | 16 | - The full stack trace and output when running `spaceship`. 17 | - The command and parameters you used to launch it. 18 | 19 | ### By providing this information it's much faster and easier to help you 20 | 21 | 22 | ## Pull Requests 23 | 24 | Pull requests are always welcome :) 25 | 26 | - Your code editor should use the tab spaces of 2 27 | - Make sure to test the changes yourself before submitting 28 | -------------------------------------------------------------------------------- /spec/portal/fixtures/addDeviceResponse.action.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | device 6 | 7 | deviceId 8 | M2WM9F639U 9 | name 10 | Demo Device 11 | deviceNumber 12 | 7f6c8dc83d77134b5a3a1c53f1202b395b04482b 13 | devicePlatform 14 | ios 15 | status 16 | c 17 | 18 | creationTimestamp 19 | 2015-05-24T18:20:31Z 20 | resultCode 21 | 0 22 | userLocale 23 | en_US 24 | protocolVersion 25 | QH65B2 26 | requestId 27 | 116282DE-76A9-4CE3-A18C-9D0290123C42 28 | requestUrl 29 | https://developerservices2.apple.com:443//services/QH65B2/ios/addDevice.action?clientId=XABBG36SBA 30 | responseId 31 | 09d5e0c4-ecd8-46d4-8c55-e2229a52073c 32 | 33 | 34 | -------------------------------------------------------------------------------- /spec/portal/fixtures/enterprise/listCertRequests.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-05-25T10:04:54Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/certificate/listCertRequests.action", 8 | "responseId": "e6c17258-fc94-458a-aa52-eb40a1d161aa", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": true, 12 | "pageNumber": 1, 13 | "pageSize": 500, 14 | "totalRecords": 1, 15 | "httpResponseHeaders": null, 16 | "certRequests": [{ 17 | "certRequestId": "46WP95RLHX", 18 | "name": "SunApps GmbH", 19 | "statusString": "Issued", 20 | "dateRequestedString": "May 12, 2014", 21 | "dateRequested": "2014-05-12T07:08:36Z", 22 | "dateCreated": "2014-05-12T07:08:40Z", 23 | "expirationDate": "2017-05-11T06:58:40Z", 24 | "expirationDateString": "May 10, 2017", 25 | "ownerType": "team", 26 | "ownerName": "SunApps GmbH", 27 | "ownerId": "YVK2GRT6A2", 28 | "canDownload": true, 29 | "canRevoke": true, 30 | "certificateId": "Q82WC5JRE9", 31 | "certificateStatusCode": 0, 32 | "certRequestStatusCode": 4, 33 | "certificateTypeDisplayId": "9RQEK7MSXA", 34 | "serialNum": "6018059F04495BAA", 35 | "typeString": "iOS Distribution" 36 | }] 37 | } -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'coveralls' 3 | Coveralls.wear! unless ENV["FASTLANE_SKIP_UPDATE_CHECK"] 4 | 5 | require 'spaceship' 6 | require 'portal/portal_stubbing' 7 | require 'tunes/tunes_stubbing' 8 | require 'du/du_stubbing' 9 | require 'plist' 10 | require 'pry' 11 | 12 | SimpleCov.at_exit do 13 | puts "Coverage done" 14 | SimpleCov.result.format! 15 | end 16 | 17 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 18 | SimpleCov::Formatter::HTMLFormatter, 19 | Coveralls::SimpleCov::Formatter 20 | ] 21 | 22 | SimpleCov.start 23 | 24 | # This module is only used to check the environment is currently a testing env 25 | module SpecHelper 26 | end 27 | 28 | ENV["DELIVER_USER"] = "spaceship@krausefx.com" 29 | ENV["DELIVER_PASSWORD"] = "so_secret" 30 | ENV.delete("FASTLANE_USER") # just in case the dev env has it 31 | 32 | unless ENV["DEBUG"] 33 | $stdout = File.open("/tmp/spaceship_tests", "w") 34 | end 35 | 36 | cache_paths = [ 37 | "/tmp/spaceship_api_key.txt", 38 | "/tmp/spaceship_itc_login_url.txt" 39 | ] 40 | 41 | def try_delete(path) 42 | FileUtils.rm_f path if File.exist? path 43 | end 44 | 45 | RSpec.configure do |config| 46 | config.before(:each) do 47 | cache_paths.each { |path| try_delete path } 48 | end 49 | 50 | config.after(:each) do 51 | cache_paths.each { |path| try_delete path } 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/spaceship.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | class << self 4 | # This client stores the default client when using the lazy syntax 5 | # Spaceship.app instead of using the spaceship launcher 6 | attr_accessor :client 7 | 8 | # Authenticates with Apple's web services. This method has to be called once 9 | # to generate a valid session. The session will automatically be used from then 10 | # on. 11 | # 12 | # This method will automatically use the username from the Appfile (if available) 13 | # and fetch the password from the Keychain (if available) 14 | # 15 | # @param user (String) (optional): The username (usually the email address) 16 | # @param password (String) (optional): The password 17 | # 18 | # @raise InvalidUserCredentialsError: raised if authentication failed 19 | # 20 | # @return (Spaceship::Client) The client the login method was called for 21 | def login(user = nil, password = nil) 22 | @client = TunesClient.login(user, password) 23 | end 24 | 25 | # Open up the team selection for the user (if necessary). 26 | # 27 | # If the user is in multiple teams, a team selection is shown. 28 | # The user can then select a team by entering the number 29 | def select_team 30 | @client.select_team 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/app_image.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | # Represents an image hosted on iTunes Connect. Used for icons, screenshots, etc 4 | class AppImage < TunesBase 5 | HOST_URL = "https://is1-ssl.mzstatic.com/image/thumb" 6 | 7 | attr_accessor :asset_token 8 | 9 | attr_accessor :sort_order 10 | 11 | attr_accessor :original_file_name 12 | 13 | attr_accessor :url 14 | 15 | attr_mapping( 16 | 'assetToken' => :asset_token, 17 | 'sortOrder' => :sort_order, 18 | 'url' => :url, 19 | 'originalFileName' => :original_file_name 20 | ) 21 | 22 | class << self 23 | def factory(attrs) 24 | self.new(attrs) 25 | end 26 | end 27 | 28 | def reset!(attrs = {}) 29 | update_raw_data!( 30 | { 31 | asset_token: nil, 32 | original_file_name: nil, 33 | sort_order: nil, 34 | url: nil 35 | }.merge(attrs) 36 | ) 37 | end 38 | 39 | def setup 40 | # Since September 2015 we don't get the url any more, so we have to manually build it 41 | self.url = "#{HOST_URL}/#{self.asset_token}/0x0ss.jpg" 42 | end 43 | 44 | private 45 | 46 | def update_raw_data!(hash) 47 | hash.each do |k, v| 48 | self.send("#{k}=", v) 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/testers/existing_internal_testers.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "users": [{ 7 | "testing": { 8 | "value": false, 9 | "isEditable": true, 10 | "isRequired": false, 11 | "errorKeys": null 12 | }, 13 | "id": null, 14 | "testerId": "1d167b89-13c5-4dd8-b988-7a6a0190f774", 15 | "firstName": { 16 | "value": "Felix", 17 | "isEditable": true, 18 | "isRequired": false, 19 | "errorKeys": null 20 | }, 21 | "lastName": { 22 | "value": "Krause", 23 | "isEditable": true, 24 | "isRequired": false, 25 | "errorKeys": null 26 | }, 27 | "emailAddress": { 28 | "value": "felix@sunapps.net", 29 | "isEditable": true, 30 | "isRequired": true, 31 | "errorKeys": null 32 | }, 33 | "status": null, 34 | "statusModTime": null, 35 | "sessions": null, 36 | "crashes": null, 37 | "latestBuild": null, 38 | "latestInstallByPlatform": {}, 39 | "groups": null 40 | }], 41 | "maxTestingUsers": 25, 42 | "addingWillInvite": false, 43 | "addingWillInviteByPlatform": null, 44 | "groups": null 45 | }, 46 | "messages": { 47 | "warn": null, 48 | "error": null, 49 | "info": null 50 | }, 51 | "statusCode": "SUCCESS" 52 | } -------------------------------------------------------------------------------- /spec/portal/fixtures/listDevicesiPhone.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-25T00:02:42Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443/services-account/QH65B2/account/ios/device/listDevices.action", 8 | "responseId": "21bcfffa-90cb-4e2b-8143-a20dca8a60b5", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": 1, 13 | "pageSize": 100, 14 | "totalRecords": 3, 15 | "devices": [ 16 | { 17 | "deviceId": "AAAAAAAAAA", 18 | "name": "Felix's iPhone", 19 | "deviceNumber": "a03b816861e89fac0a4da5884cb9d2f01bd5641e", 20 | "devicePlatform": "ios", 21 | "status": "c", 22 | "model": "iPhone 5 (Model A1428)", 23 | "deviceClass": "iphone" 24 | }, 25 | { 26 | "deviceId": "BBBBBBBBBB", 27 | "name": "Stefan's iPhone", 28 | "deviceNumber": "e5814abb3b1d92087d48b64f375d8e7694932c39", 29 | "devicePlatform": "ios", 30 | "status": "c", 31 | "model": "iPhone 6", 32 | "deviceClass": "iphone" 33 | }, 34 | { 35 | "deviceId": "DDDDDDDDDD", 36 | "name": "iPhone", 37 | "deviceNumber": "51dda99a3efe92ffc7d8e9dfe4ca4d2fb94ae509", 38 | "devicePlatform": "ios", 39 | "status": "c", 40 | "model": "iPhone 4 GSM", 41 | "deviceClass": "iphone" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /spec/portal/fixtures/certificateCreate.certRequest.json: -------------------------------------------------------------------------------- 1 | { 2 | "certRequestId": "ESF52FQGU6", 3 | "statusCode": 4, 4 | "statusString": "Issued", 5 | "csrPlatform": "ios", 6 | "dateRequestedString": "Apr 29, 2015", 7 | "dateRequested": "2015-04-29T16:07:11Z", 8 | "expirationDate": "2016-04-28T15:57:11Z", 9 | "expirationDateString": "Apr 28, 2016", 10 | "certificateId": "HG3FQ9M282", 11 | "typeString": "APNs Production iOS", 12 | "certificateType": { 13 | "certificateTypeDisplayId": "UPV3DW712I", 14 | "name": "APNs Production iOS", 15 | "platform": "ios", 16 | "permissionType": "production", 17 | "distributionType": "production", 18 | "distributionMethod": "push", 19 | "ownerType": "bundle", 20 | "daysOverlap": 364, 21 | "maxActive": 2 22 | }, 23 | "certificate": { 24 | "name": null, 25 | "certificateId": "HG3FQ9M282", 26 | "serialNumber": "7C4B29A2943BACA0", 27 | "status": "Issued", 28 | "statusCode": 0, 29 | "expirationDate": "2016-04-28", 30 | "certificatePlatform": "ios", 31 | "certificateType": { 32 | "certificateTypeDisplayId": "UPV3DW712I", 33 | "name": "APNs Production iOS", 34 | "platform": "ios", 35 | "permissionType": "production", 36 | "distributionType": "production", 37 | "distributionMethod": "push", 38 | "ownerType": "bundle", 39 | "daysOverlap": 364, 40 | "maxActive": 2 41 | }, 42 | "hasAskKey": false 43 | } 44 | } -------------------------------------------------------------------------------- /spec/portal/fixtures/downloaded_provisioning_profile.mobileprovision: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AppIDName 5 | SunApp Setup 6 | ApplicationIdentifierPrefix 7 | 8 | 5A997XSHK2 9 | 10 | CreationDate 11 | 2015-02-21T15:57:07Z 12 | DeveloperCertificates 13 | 14 | REDACTED 15 | 16 | Entitlements 17 | 18 | keychain-access-groups 19 | 20 | 5A997XSHK2.* 21 | 22 | get-task-allow 23 | 24 | application-identifier 25 | 5A997XSHK2.net.sunapps.9 26 | com.apple.developer.team-identifier 27 | 5A997XSHK2 28 | aps-environment 29 | production 30 | beta-reports-active 31 | 32 | 33 | 34 | ExpirationDate 35 | 2016-02-10T23:44:20Z 36 | Name 37 | App Name 38 | TeamIdentifier 39 | 40 | 5A997XSHK2 41 | 42 | TeamName 43 | SunApps GmbH 44 | TimeToLive 45 | 354 46 | UUID 47 | REDACRED 48 | Version 49 | 1 50 | 51 | -------------------------------------------------------------------------------- /spec/spaceship_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship do 4 | before { Spaceship.login } 5 | 6 | it "#select_team" do 7 | expect(Spaceship.select_team).to eq('XXXXXXXXXX') 8 | end 9 | 10 | it 'should initialize with a client' do 11 | expect(Spaceship.client).to be_instance_of(Spaceship::PortalClient) 12 | end 13 | 14 | it "Device" do 15 | expect(Spaceship.device.all.count).to eq(4) 16 | end 17 | 18 | it "Certificate" do 19 | expect(Spaceship.certificate.all.count).to eq(3) 20 | end 21 | 22 | it "ProvisioningProfile" do 23 | expect(Spaceship.provisioning_profile.all.count).to eq(33) 24 | end 25 | 26 | it "App" do 27 | expect(Spaceship.app.all.count).to eq(5) 28 | end 29 | 30 | it "App Group" do 31 | expect(Spaceship.app_group.all.count).to eq(2) 32 | end 33 | 34 | describe Spaceship::Launcher do 35 | it 'has a client' do 36 | expect(subject.client).to be_instance_of(Spaceship::PortalClient) 37 | end 38 | 39 | it 'returns a scoped model class' do 40 | expect(subject.app).to eq(Spaceship::App) 41 | expect(subject.app_group).to eq(Spaceship::AppGroup) 42 | expect(subject.certificate).to eq(Spaceship::Certificate) 43 | expect(subject.device).to eq(Spaceship::Device) 44 | expect(subject.provisioning_profile).to eq(Spaceship::ProvisioningProfile) 45 | end 46 | 47 | it 'passes the client to the models' do 48 | expect(subject.device.client).to eq(subject.client) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/portal/fixtures/addAppId.action.wildcard.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-29T00:28:48Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/identifiers/addAppId.action", 8 | "responseId": "278f3ddc-4ae1-40b0-8b79-a7555a31e879", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "appId": { 16 | "appIdId": "7Z947A8XYJ", 17 | "name": "Development App", 18 | "appIdPlatform": "ios", 19 | "prefix": "S56PJTAYEL", 20 | "identifier": "tools.fastlane.spaceship.*", 21 | "isWildCard": true, 22 | "isDuplicate": false, 23 | "features": { 24 | "push": false, 25 | "inAppPurchase": false, 26 | "gameCenter": false, 27 | "passbook": false, 28 | "dataProtection": "", 29 | "homeKit": false, 30 | "cloudKitVersion": 1, 31 | "iCloud": false, 32 | "LPLF93JG7M": false, 33 | "IAD53UNK2F": false, 34 | "V66P55NK2I": false, 35 | "SKC3T5S89Y": false, 36 | "APG3427HIY": false, 37 | "HK421J6T7P": false, 38 | "WC421J6T7P": false 39 | }, 40 | "enabledFeatures": [ 41 | 42 | ], 43 | "isDevPushEnabled": false, 44 | "isProdPushEnabled": false, 45 | "associatedApplicationGroupsCount": null, 46 | "associatedCloudContainersCount": null, 47 | "associatedIdentifiersCount": null 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /spec/portal/fixtures/list_certificates_filtered.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-04T09:41:22Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/certificate/listCertRequests.action?teamId=5A997XSHK2&types=5QPB9NHCEI", 8 | "responseId": "15e02715-5535-4249-8c06-1e2c32f0aaaa", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": true, 12 | "pageNumber": 0, 13 | "pageSize": 0, 14 | "totalRecords": 1, 15 | "certRequests": [ 16 | { 17 | "certRequestId": "2P768T2AAA", 18 | "name": "Felix Krause", 19 | "statusString": "Issued", 20 | "dateRequestedString": "Nov 25, 2014", 21 | "dateRequested": "2014-11-25T22:55:42Z", 22 | "dateCreated": "2014-11-25T22:55:50Z", 23 | "expirationDate": "2015-11-25T22:45:50Z", 24 | "expirationDateString": "Nov 25, 2015", 25 | "ownerType": "teamMember", 26 | "ownerName": "Felix Krause", 27 | "ownerId": "5Y354CXU3A", 28 | "canDownload": true, 29 | "canRevoke": true, 30 | "certificateId": "C8DL7464RQ", 31 | "certificateStatusCode": 0, 32 | "certRequestStatusCode": 4, 33 | "certificateTypeDisplayId": "5QPB9NHAAA", 34 | "serialNum": "1B58B611A525FAAA", 35 | "typeString": "iOS Development" 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /spec/portal/fixtures/addAppId.action.explicit.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-29T00:26:33Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/identifiers/addAppId.action", 8 | "responseId": "ed5e74cf-8f52-46bf-9cf6-9e9e9fcd9fad", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "appId": { 16 | "appIdId": "2HNR359G63", 17 | "name": "Production App", 18 | "appIdPlatform": "ios", 19 | "prefix": "S56PJTAYEL", 20 | "identifier": "tools.fastlane.spaceship.some-explicit-app", 21 | "isWildCard": false, 22 | "isDuplicate": false, 23 | "features": { 24 | "push": true, 25 | "inAppPurchase": true, 26 | "gameCenter": true, 27 | "passbook": false, 28 | "dataProtection": "", 29 | "homeKit": false, 30 | "cloudKitVersion": 1, 31 | "iCloud": false, 32 | "LPLF93JG7M": false, 33 | "IAD53UNK2F": false, 34 | "V66P55NK2I": false, 35 | "SKC3T5S89Y": false, 36 | "APG3427HIY": false, 37 | "HK421J6T7P": false, 38 | "WC421J6T7P": false 39 | }, 40 | "enabledFeatures": [ 41 | "gameCenter", 42 | "inAppPurchase", 43 | "push" 44 | ], 45 | "isDevPushEnabled": false, 46 | "isProdPushEnabled": false, 47 | "associatedApplicationGroupsCount": null, 48 | "associatedCloudContainersCount": null, 49 | "associatedIdentifiersCount": null 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/spaceship/du/upload_file.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Spaceship 4 | # a wrapper around the concept of file required to make uploads to DU 5 | class UploadFile 6 | attr_reader :file_path 7 | attr_reader :file_name 8 | attr_reader :file_size 9 | attr_reader :content_type 10 | attr_reader :bytes 11 | 12 | class << self 13 | def from_path(path) 14 | raise "Image must exists at path: #{path}" unless File.exist?(path) 15 | path = remove_alpha_channel(path) if File.extname(path).downcase == '.png' 16 | 17 | content_type = Utilities.content_type(path) 18 | self.new( 19 | file_path: path, 20 | file_name: File.basename(path), 21 | file_size: File.size(path), 22 | content_type: content_type, 23 | bytes: File.read(path) 24 | ) 25 | end 26 | 27 | # As things like screenshots and app icon shouldn't contain the alpha channel 28 | # This will copy the image into /tmp to remove the alpha channel there 29 | # That's done to not edit the original image 30 | def remove_alpha_channel(original) 31 | path = "/tmp/#{Digest::MD5.hexdigest(original)}.png" 32 | FileUtils.copy(original, path) 33 | `sips -s format bmp '#{path}' &> /dev/null ` # &> /dev/null since there is warning because of the extension 34 | `sips -s format png '#{path}'` 35 | return path 36 | end 37 | end 38 | 39 | private 40 | 41 | def initialize(args) 42 | args.each do |k, v| 43 | instance_variable_set("@#{k}", v) unless v.nil? 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /bin/spaceship: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.push File.expand_path("../../lib", __FILE__) 3 | 4 | require "spaceship" 5 | require "pry" 6 | require "colored" 7 | require "credentials_manager" 8 | 9 | def docs 10 | url = 'https://github.com/fastlane/spaceship/tree/master/docs' 11 | `open '#{url}'` 12 | url 13 | end 14 | 15 | username = ARGV[1] if ARGV[0] == '-u' 16 | username ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id) 17 | username ||= ask("Username: ") 18 | 19 | begin 20 | puts "Logging into to iTunes Connect (#{username})..." 21 | Spaceship::Tunes.login(username) 22 | puts "Successfully logged in to iTunes Connect".green 23 | puts "" 24 | rescue 25 | puts "Could not login to iTunes Connect..." 26 | end 27 | begin 28 | puts "Logging into the Developer Portal (#{username})..." 29 | Spaceship::Portal.login(username) 30 | puts "Successfully logged in to the Developer Portal".green 31 | puts "" 32 | rescue 33 | puts "Could not login to the Developer Portal..." 34 | end 35 | 36 | puts "---------------------------------------".green 37 | puts "| Welcome to the spaceship playground |".green 38 | puts "---------------------------------------".green 39 | puts "" 40 | puts "Enter #{'docs'.yellow} to open up the documentation" 41 | puts "Enter #{'exit'.yellow} to exit the spaceship playground" 42 | puts "Enter #{'_'.yellow} to access the return value of the last executed command" 43 | puts "" 44 | puts "Just enter the commands and confirm with Enter".green 45 | 46 | # rubocop:disable Lint/Debugger 47 | binding.pry(quiet: true) 48 | # rubocop:enable Lint/Debugger 49 | 50 | puts "" # Fixes https://github.com/fastlane/spaceship/issues/203 51 | -------------------------------------------------------------------------------- /spec/portal/fixtures/listDevices.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-25T00:02:42Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443/services-account/QH65B2/account/ios/device/listDevices.action", 8 | "responseId": "21bcfffa-90cb-4e2b-8143-a20dca8a60b5", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": 1, 13 | "pageSize": 100, 14 | "totalRecords": 4, 15 | "devices": [ 16 | { 17 | "deviceId": "AAAAAAAAAA", 18 | "name": "Felix's iPhone", 19 | "deviceNumber": "a03b816861e89fac0a4da5884cb9d2f01bd5641e", 20 | "devicePlatform": "ios", 21 | "status": "c", 22 | "model": "iPhone 5 (Model A1428)", 23 | "deviceClass": "iphone" 24 | }, 25 | { 26 | "deviceId": "BBBBBBBBBB", 27 | "name": "Stefan's iPhone", 28 | "deviceNumber": "e5814abb3b1d92087d48b64f375d8e7694932c39", 29 | "devicePlatform": "ios", 30 | "status": "c", 31 | "model": "iPhone 6", 32 | "deviceClass": "iphone" 33 | }, 34 | { 35 | "deviceId": "CCCCCCCCCC", 36 | "name": "Personal iPhone", 37 | "deviceNumber": "97467684eb8dfa3c6d272eac3890dab0d001c706", 38 | "devicePlatform": "ios", 39 | "status": "c", 40 | "model": null, 41 | "deviceClass": "ipod" 42 | }, 43 | { 44 | "deviceId": "DDDDDDDDDD", 45 | "name": "iPhone", 46 | "deviceNumber": "51dda99a3efe92ffc7d8e9dfe4ca4d2fb94ae509", 47 | "devicePlatform": "ios", 48 | "status": "c", 49 | "model": "iPhone 4 GSM", 50 | "deviceClass": "iphone" 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/language_item.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | # Represents one attribute (e.g. name) of an app in multiple languages 4 | class LanguageItem 5 | attr_accessor :identifier # title or description 6 | attr_accessor :original_array # reference to original array 7 | 8 | def initialize(identifier, ref) 9 | raise "ref is nil" if ref.nil? 10 | 11 | self.identifier = identifier.to_s 12 | self.original_array = ref 13 | end 14 | 15 | def [](key) 16 | get_lang(key)[identifier]['value'] 17 | end 18 | 19 | def []=(key, value) 20 | get_lang(key)[identifier]['value'] = value 21 | end 22 | 23 | def get_lang(lang) 24 | result = self.original_array.find do |current| 25 | current['language'] == lang or current['localeCode'] == lang # Apple being consistent 26 | end 27 | return result if result 28 | 29 | raise "Language '#{lang}' is not activated for this app version." 30 | end 31 | 32 | # @return (Array) An array containing all languages that are already available 33 | def keys 34 | self.original_array.collect { |l| l.fetch('language') } 35 | end 36 | 37 | # @return (Array) An array containing all languages that are already available 38 | # alias for keys 39 | def languages 40 | keys 41 | end 42 | 43 | def inspect 44 | result = "" 45 | self.original_array.collect do |current| 46 | result += "#{current.fetch('language')}: #{current.fetch(identifier, {}).fetch('value')}\n" 47 | end 48 | result 49 | end 50 | 51 | def to_s 52 | inspect 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/portal/fixtures/getAppIdDetail.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-06-16T07:47:39Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": "8ac49b41-3e33-4ad7-a4ec-11b8fec30b45", 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/identifiers/getAppIdDetail.action", 8 | "responseId": "348c13a8-fa35-4cac-b49e-09366fa1fa04", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": true, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "httpResponseHeaders": null, 16 | "appId": { 17 | "appIdId": "B7JBD8LHAA", 18 | "name": "The App Name", 19 | "appIdPlatform": "ios", 20 | "prefix": "5A997XSHK2", 21 | "identifier": "net.sunapps.151", 22 | "isWildCard": false, 23 | "isDuplicate": false, 24 | "features": { 25 | "push": true, 26 | "inAppPurchase": true, 27 | "gameCenter": true, 28 | "passbook": false, 29 | "dataProtection": "", 30 | "homeKit": false, 31 | "cloudKitVersion": 1, 32 | "iCloud": false, 33 | "LPLF93JG7M": false, 34 | "IAD53UNK2F": false, 35 | "V66P55NK2I": false, 36 | "SKC3T5S89Y": false, 37 | "APG3427HIY": false, 38 | "HK421J6T7P": false, 39 | "WC421J6T7P": false 40 | }, 41 | "enabledFeatures": ["gameCenter", "inAppPurchase", "push"], 42 | "isDevPushEnabled": false, 43 | "isProdPushEnabled": true, 44 | "associatedApplicationGroupsCount": 0, 45 | "associatedCloudContainersCount": 0, 46 | "associatedIdentifiersCount": 0 47 | } 48 | } -------------------------------------------------------------------------------- /spec/tunes/app_details_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Tunes::AppDetails do 4 | before { Spaceship::Tunes.login } 5 | 6 | let(:client) { Spaceship::AppVersion.client } 7 | let(:app) { Spaceship::Application.all.first } 8 | 9 | describe "App Details are properly loaded" do 10 | it "contains all the relevant information" do 11 | details = app.details 12 | 13 | expect(details.name['en-US']).to eq("Updated by fastlane") 14 | expect(details.privacy_url['en-US']).to eq('https://fastlane.tools') 15 | expect(details.primary_category).to eq('MZGenre.Sports') 16 | end 17 | end 18 | 19 | describe "Modifying the app category" do 20 | it "prefixes the category with the correct value for all category types" do 21 | details = app.details 22 | 23 | details.primary_category = "Weather" 24 | expect(details.primary_category).to eq("MZGenre.Weather") 25 | 26 | details.primary_first_sub_category = "Weather" 27 | expect(details.primary_first_sub_category).to eq("MZGenre.Weather") 28 | 29 | details.primary_second_sub_category = "Weather" 30 | expect(details.primary_second_sub_category).to eq("MZGenre.Weather") 31 | 32 | details.secondary_category = "Weather" 33 | expect(details.secondary_category).to eq("MZGenre.Weather") 34 | 35 | details.secondary_first_sub_category = "Weather" 36 | expect(details.secondary_first_sub_category).to eq("MZGenre.Weather") 37 | 38 | details.secondary_second_sub_category = "Weather" 39 | expect(details.secondary_second_sub_category).to eq("MZGenre.Weather") 40 | end 41 | 42 | it "doesn't prefix if the prefix is already there" do 43 | details = app.details 44 | 45 | details.primary_category = "MZGenre.Weather" 46 | expect(details.primary_category).to eq("MZGenre.Weather") 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/portal/fixtures/listTeams.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-24T23:33:38Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/listTeams.action", 8 | "responseId": "6f4c5dcb-ecd0-460c-967b-dcd81f7be315", 9 | "isAdmin": null, 10 | "isMember": null, 11 | "isAgent": null, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "teams": [ 16 | { 17 | "status": "active", 18 | "teamId": "XXXXXXXXXX", 19 | "type": "Company/Organization", 20 | "extendedTeamAttributes": { 21 | }, 22 | "teamAgent": { 23 | "personId": 499707568, 24 | "firstName": "Felix", 25 | "lastName": "Krause", 26 | "email": "test@spaceship.com", 27 | "developerStatus": "active", 28 | "teamMemberId": "YYYYYYYYYY" 29 | }, 30 | "memberships": [ 31 | { 32 | "membershipId": "MMMMMMMMMM", 33 | "membershipProductId": "ds1", 34 | "status": "active", 35 | "inDeviceResetWindow": true, 36 | "inRenewalWindow": true, 37 | "dateStart": "02/23/15 03:38", 38 | "dateExpire": "02/23/16 07:59", 39 | "platform": "ios", 40 | "availableDeviceSlots": 100, 41 | "name": "iOS Developer Program" 42 | } 43 | ], 44 | "currentTeamMember": { 45 | "personId": null, 46 | "firstName": null, 47 | "lastName": null, 48 | "email": null, 49 | "developerStatus": null, 50 | "privileges": { 51 | }, 52 | "roles": [ 53 | "TEAM_ADMIN", 54 | "SAFARI_PROGRAM_TEAM_ADMIN" 55 | ], 56 | "teamMemberId": "YYYYYYYYYY" 57 | }, 58 | "name": "SpaceShip" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /spec/launcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship do 4 | describe Spaceship::Launcher do 5 | let(:username) { 'spaceship@krausefx.com' } 6 | let(:password) { 'so_secret' } 7 | let(:spaceship1) { Spaceship::Launcher.new } 8 | let(:spaceship2) { Spaceship::Launcher.new } 9 | 10 | before do 11 | spaceship1.login(username, password) 12 | spaceship2.login(username, password) 13 | end 14 | 15 | it 'should have 2 separate spaceships' do 16 | expect(spaceship1).to_not eq(spaceship2) 17 | end 18 | 19 | it '#select_team' do 20 | expect(spaceship1.select_team).to eq("XXXXXXXXXX") 21 | end 22 | 23 | it "may have different teams" do 24 | team_id = "ABCDEF" 25 | spaceship1.client.team_id = team_id 26 | 27 | expect(spaceship1.client.team_id).to eq(team_id) # custom 28 | expect(spaceship2.client.team_id).to eq("XXXXXXXXXX") # default 29 | end 30 | 31 | it "Device" do 32 | expect(spaceship1.device.all.count).to eq(4) 33 | end 34 | 35 | it "Certificate" do 36 | expect(spaceship1.certificate.all.count).to eq(3) 37 | end 38 | 39 | it "ProvisioningProfile" do 40 | expect(spaceship1.provisioning_profile.all.count).to eq(33) 41 | end 42 | 43 | it "App" do 44 | expect(spaceship1.app.all.count).to eq(5) 45 | end 46 | 47 | context "With an uninitialized environment" do 48 | before do 49 | Spaceship::App.set_client(nil) 50 | Spaceship::AppGroup.set_client(nil) 51 | Spaceship::Device.set_client(nil) 52 | Spaceship::Certificate.set_client(nil) 53 | Spaceship::ProvisioningProfile.set_client(nil) 54 | end 55 | it "shouldn't fail if provisioning_profile is invoked before app and device" do 56 | clean_launcher = Spaceship::Launcher.new 57 | clean_launcher.login(username, password) 58 | expect(clean_launcher.provisioning_profile.all.count).to eq(33) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/portal/fixtures/listDevicesPage1-2.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-05-24T16:37:32Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/device/listDevices.action", 8 | "responseId": "881a1f34-bc13-425b-ae18-9f4f8fb40aaa", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": true, 12 | "pageNumber": 1, 13 | "pageSize": 8, 14 | "totalRecords": 9, 15 | "httpResponseHeaders": null, 16 | "devices": [{ 17 | "deviceId": "9T5RA84AAA", 18 | "name": "Felix Krause's iPhone 6", 19 | "deviceNumber": "aaabbbccccddddaaabbb", 20 | "devicePlatform": "ios", 21 | "status": "c" 22 | }, { 23 | "deviceId": "L4378H2AAA", 24 | "name": "Felix's iPad", 25 | "deviceNumber": "aaabbbccccddddaaabbb", 26 | "devicePlatform": "ios", 27 | "status": "c" 28 | }, { 29 | "deviceId": "LEL449RAAA", 30 | "name": "4s", 31 | "deviceNumber": "aaabbbccccddddaaabbb", 32 | "devicePlatform": "ios", 33 | "status": "c" 34 | }, { 35 | "deviceId": "S4227Y4AAA", 36 | "name": "Simon iOS", 37 | "deviceNumber": "aaabbbccccddddaaabbb", 38 | "devicePlatform": "ios", 39 | "status": "c" 40 | }, { 41 | "deviceId": "5YTNZ5AAAA", 42 | "name": "iPhone 4S", 43 | "deviceNumber": "aaabbbccccddddaaabbb", 44 | "devicePlatform": "ios", 45 | "status": "c" 46 | }, { 47 | "deviceId": "WXQ7V239BE", 48 | "name": "iPhone 4s", 49 | "deviceNumber": "aaabbbccccddddaaabbb", 50 | "devicePlatform": "ios", 51 | "status": "c" 52 | }, { 53 | "deviceId": "9K3WR66AAA", 54 | "name": "MyName", 55 | "deviceNumber": "aaabbbccccddddaaabbb", 56 | "devicePlatform": "ios", 57 | "status": "c" 58 | }, { 59 | "deviceId": "GD25LDGAAA", 60 | "name": "iPhonekajsdf", 61 | "deviceNumber": "aaabbbccccddddaaabbb", 62 | "devicePlatform": "ios", 63 | "status": "c" 64 | }] 65 | } -------------------------------------------------------------------------------- /spec/portal/fixtures/revokeCertificate.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-05-04T19:11:25Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": "9fb55ba7-dd61-4bae-88b4-9b4c9662225e", 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/certificate/revokeCertificate.action", 8 | "responseId": "f92ef71b-5448-4741-bdbb-72db40fdebac", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "certRequests": [ 16 | { 17 | "certRequestId": "URG8EFZ96G", 18 | "name": "Revocation", 19 | "statusCode": 1, 20 | "statusString": "Approved", 21 | "dateRequestedString": "May 04, 2015", 22 | "dateRequested": "2015-05-04T19:11:25Z", 23 | "expirationDate": "2016-04-28T20:14:05Z", 24 | "expirationDateString": "Apr 28, 2016", 25 | "certificateId": "WHT3M5V55A", 26 | "typeString": "Revocation", 27 | "certificateType": { 28 | "certificateTypeDisplayId": "W147BDC2UZ", 29 | "name": "Revocation", 30 | "daysOverlap": 0, 31 | "maxActive": 0 32 | }, 33 | "certificate": { 34 | "name": null, 35 | "certificateId": "WHT3M5V55A", 36 | "serialNumber": "73F7E6F18B54DD37", 37 | "status": "Revocation Pending", 38 | "statusCode": 2, 39 | "expirationDate": "2016-04-28", 40 | "certificatePlatform": "ios", 41 | "certificateType": { 42 | "certificateTypeDisplayId": "UPV3DW712I", 43 | "name": "APNs Production iOS", 44 | "platform": "ios", 45 | "permissionType": "distribution", 46 | "distributionType": "distribution", 47 | "distributionMethod": "push", 48 | "ownerType": "bundle", 49 | "daysOverlap": 364, 50 | "maxActive": 2 51 | }, 52 | "hasAskKey": false 53 | } 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/app_trailer.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | # Represents a preview video hosted on iTunes Connect. Used for icons, screenshots, etc 4 | class AppTrailer < TunesBase 5 | attr_accessor :video_asset_token 6 | 7 | attr_accessor :picture_asset_token 8 | 9 | attr_accessor :descriptionXML 10 | 11 | attr_accessor :preview_frame_time_code 12 | 13 | attr_accessor :video_url 14 | 15 | attr_accessor :preview_image_url 16 | 17 | attr_accessor :full_sized_preview_image_url 18 | 19 | attr_accessor :device_type 20 | 21 | attr_accessor :language 22 | 23 | attr_mapping( 24 | 'videoAssetToken' => :video_asset_token, 25 | 'pictureAssetToken' => :picture_asset_token, 26 | 'descriptionXML' => :descriptionXML, 27 | 'previewFrameTimeCode' => :preview_frame_time_code, 28 | 'isPortrait' => :is_portrait, 29 | 'videoUrl' => :video_url, 30 | 'previewImageUrl' => :preview_image_url, 31 | 'fullSizedPreviewImageUrl' => :full_sized_preview_image_url, 32 | 'contentType' => :content_type, 33 | 'videoStatus' => :video_status 34 | ) 35 | 36 | class << self 37 | def factory(attrs) 38 | self.new(attrs) 39 | end 40 | end 41 | 42 | def reset!(attrs = {}) 43 | update_raw_data! 44 | ({ 45 | video_asset_token: nil, 46 | picture_asset_token: nil, 47 | descriptionXML: nil, 48 | preview_frame_time_code: nil, 49 | is_portrait: nil, 50 | video_url: nil, 51 | preview_image_url: nil, 52 | full_sized_preview_image_url: nil, 53 | content_type: nil, 54 | video_status: nil, 55 | device_type: nil, 56 | language: nil 57 | }.merge(attrs) 58 | ) 59 | end 60 | 61 | private 62 | 63 | def update_raw_data!(hash) 64 | hash.each do |k, v| 65 | self.send("#{k}=", v) 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/tunes/tunes_client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::TunesClient do 4 | describe '#login' do 5 | it 'raises an exception if authentication failed' do 6 | expect do 7 | subject.login('bad-username', 'bad-password') 8 | end.to raise_exception(Spaceship::Client::InvalidUserCredentialsError, "Invalid username and password combination. Used 'bad-username' as the username.") 9 | end 10 | end 11 | 12 | describe 'client' do 13 | it 'exposes the session cookie' do 14 | begin 15 | subject.login('bad-username', 'bad-password') 16 | rescue Spaceship::Client::InvalidUserCredentialsError 17 | expect(subject.cookie).to eq('session=invalid') 18 | end 19 | end 20 | end 21 | 22 | describe "Logged in" do 23 | subject { Spaceship::Tunes.client } 24 | let(:username) { 'spaceship@krausefx.com' } 25 | let(:password) { 'so_secret' } 26 | 27 | before do 28 | Spaceship::Tunes.login(username, password) 29 | end 30 | 31 | it 'stores the username' do 32 | expect(subject.user).to eq('spaceship@krausefx.com') 33 | end 34 | 35 | it "#hostname" do 36 | expect(subject.class.hostname).to eq('https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/') 37 | end 38 | 39 | describe "#handle_itc_response" do 40 | it "raises an exception if something goes wrong" do 41 | data = JSON.parse(itc_read_fixture_file('update_app_version_failed.json'))['data'] 42 | expect do 43 | subject.handle_itc_response(data) 44 | end.to raise_error "The App Name you entered has already been used. The App Name you entered has already been used. You must provide an address line. There are errors on the page and for 2 of your localizations." 45 | end 46 | 47 | it "does nothing if everything works as expected and returns the original data" do 48 | data = JSON.parse(itc_read_fixture_file('update_app_version_success.json'))['data'] 49 | expect(subject.handle_itc_response(data)).to eq(data) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spaceship.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'spaceship/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "spaceship" 8 | spec.version = Spaceship::VERSION 9 | spec.authors = ["Felix Krause", "Stefan Natchev"] 10 | spec.email = ["spaceship@krausefx.com", "stefan@natchev.com"] 11 | spec.summary = 'Because you would rather spend your time building stuff than fighting provisioning' 12 | spec.description = 'Because you would rather spend your time building stuff than fighting provisioning' 13 | spec.homepage = "https://fastlane.tools" 14 | spec.license = "MIT" 15 | 16 | spec.required_ruby_version = '>= 2.0.0' 17 | 18 | spec.files = Dir["lib/**/*"] + %w(bin/spaceship README.md LICENSE) 19 | 20 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 21 | 22 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 23 | spec.require_paths = ["lib"] 24 | 25 | # fastlane specific 26 | spec.add_dependency 'credentials_manager', '>= 0.9.0' # to automatically get login information 27 | 28 | # external 29 | spec.add_dependency 'multi_xml', '~> 0.5' 30 | spec.add_dependency 'plist', '~> 3.1' 31 | spec.add_dependency 'faraday', '~> 0.9' 32 | spec.add_dependency 'faraday_middleware', '~> 0.9' 33 | spec.add_dependency 'faraday-cookie_jar', '~> 0.0.6' 34 | spec.add_dependency 'fastimage', '~> 1.6' 35 | 36 | # for the playground 37 | spec.add_dependency 'pry' 38 | spec.add_dependency 'colored' 39 | 40 | # Development only 41 | spec.add_development_dependency 'bundler' 42 | spec.add_development_dependency 'fastlane', ">= 1.15.0" # yes, we use fastlane to test fastlane 43 | spec.add_development_dependency 'rake' 44 | spec.add_development_dependency 'coveralls' 45 | spec.add_development_dependency 'rspec', '~> 3.1.0' 46 | spec.add_development_dependency 'yard', '~> 0.8.7.4' 47 | spec.add_development_dependency 'webmock', '~> 1.21.0' 48 | spec.add_development_dependency 'rubocop', '~> 0.32.1' 49 | end 50 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/app_submission/complete_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "adIdInfo": { 4 | "limitsTracking": { 5 | "errorKeys": null, 6 | "isEditable": false, 7 | "isRequired": false, 8 | "value": null 9 | }, 10 | "sectionErrorKeys": [], 11 | "sectionInfoKeys": [], 12 | "sectionWarningKeys": [], 13 | "servesAds": { 14 | "errorKeys": null, 15 | "isEditable": false, 16 | "isRequired": false, 17 | "value": null 18 | }, 19 | "tracksAction": { 20 | "errorKeys": null, 21 | "isEditable": false, 22 | "isRequired": false, 23 | "value": null 24 | }, 25 | "tracksInstall": { 26 | "errorKeys": null, 27 | "isEditable": false, 28 | "isRequired": false, 29 | "value": null 30 | }, 31 | "usesIdfa": { 32 | "errorKeys": null, 33 | "isEditable": false, 34 | "isRequired": true, 35 | "value": null 36 | } 37 | }, 38 | "contentRights": null, 39 | "exportCompliance": { 40 | "appType": "iOS App", 41 | "availableOnFrenchStore": null, 42 | "ccatFile": null, 43 | "containsProprietaryCryptography": null, 44 | "containsThirdPartyCryptography": null, 45 | "encryptionUpdated": null, 46 | "exportComplianceRequired": false, 47 | "isExempt": null, 48 | "platform": "ios", 49 | "sectionErrorKeys": [], 50 | "sectionInfoKeys": [], 51 | "sectionWarningKeys": [], 52 | "usesEncryption": null 53 | }, 54 | "previousPurchaseRestrictions": null, 55 | "versionInfo": null 56 | }, 57 | "messages": { 58 | "error": null, 59 | "info": [ 60 | "Successful POST" 61 | ], 62 | "warn": null 63 | }, 64 | "statusCode": "SUCCESS" 65 | } 66 | -------------------------------------------------------------------------------- /spec/portal/app_group_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Portal::AppGroup do 4 | before { Spaceship.login } 5 | let(:client) { Spaceship::Portal::AppGroup.client } 6 | 7 | describe "successfully loads and parses all app groups" do 8 | it "the number is correct" do 9 | expect(Spaceship::Portal::AppGroup.all.count).to eq(2) 10 | end 11 | 12 | it "inspect works" do 13 | expect(Spaceship::Portal::AppGroup.all.first.inspect).to include("Portal::AppGroup") 14 | end 15 | 16 | it "parses app group correctly" do 17 | group = Spaceship::Portal::AppGroup.all.first 18 | 19 | expect(group.group_id).to eq("group.com.example.one") 20 | expect(group.name).to eq("First group") 21 | expect(group.status).to eq("current") 22 | expect(group.app_group_id).to eq("44V62UZ8L7") 23 | expect(group.prefix).to eq("9J57U9392R") 24 | end 25 | 26 | it "allows modification of values and properly retrieving them" do 27 | group = Spaceship::AppGroup.all.first 28 | group.name = "12" 29 | expect(group.name).to eq("12") 30 | end 31 | end 32 | 33 | describe "Filter app group based on group identifier" do 34 | it "works with specific App Group IDs" do 35 | group = Spaceship::Portal::AppGroup.find("group.com.example.two") 36 | expect(group.app_group_id).to eq("2GKKV64NUG") 37 | end 38 | 39 | it "returns nil app group ID wasn't found" do 40 | expect(Spaceship::Portal::AppGroup.find("asdfasdf")).to be_nil 41 | end 42 | end 43 | 44 | describe '#create' do 45 | it 'creates an app group' do 46 | expect(client).to receive(:create_app_group!).with('Production App Group', 'group.tools.fastlane').and_return({}) 47 | group = Spaceship::Portal::AppGroup.create!(group_id: 'group.tools.fastlane', name: 'Production App Group') 48 | end 49 | end 50 | 51 | describe '#delete' do 52 | subject { Spaceship::Portal::AppGroup.find("group.com.example.two") } 53 | it 'deletes the app group by a given app_group_id' do 54 | expect(client).to receive(:delete_app_group!).with('2GKKV64NUG') 55 | group = subject.delete! 56 | expect(group.app_group_id).to eq('2GKKV64NUG') 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/tunes/language_converter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Tunes::LanguageConverter do 4 | let(:klass) { Spaceship::Tunes::LanguageConverter } 5 | 6 | describe "#from_itc_to_standard" do 7 | it "works with valid inputs" do 8 | expect(klass.from_itc_to_standard('English')).to eq('en-US') 9 | expect(klass.from_itc_to_standard('English_CA')).to eq('en-CA') 10 | expect(klass.from_itc_to_standard('Brazilian Portuguese')).to eq('pt-BR') 11 | end 12 | 13 | it "returns nil when element can't be found" do 14 | expect(klass.from_itc_to_standard('asdfasdf')).to eq(nil) 15 | end 16 | end 17 | 18 | describe "#from_standard_to_itc" do 19 | it "works with valid inputs" do 20 | expect(klass.from_standard_to_itc('en-US')).to eq('English') 21 | expect(klass.from_standard_to_itc('pt-BR')).to eq('Brazilian Portuguese') 22 | end 23 | 24 | it "works with alternative values too" do 25 | expect(klass.from_standard_to_itc('de')).to eq('German') 26 | end 27 | 28 | it "returns nil when element can't be found" do 29 | expect(klass.from_standard_to_itc('asdfasdf')).to eq(nil) 30 | end 31 | end 32 | 33 | describe "from readable to value" do 34 | it "works with valid inputs" do 35 | expect(klass.from_itc_readable_to_itc('UK English')).to eq('English_UK') 36 | end 37 | 38 | it "returns nil when element doesn't exist" do 39 | expect(klass.from_itc_readable_to_itc('notHere')).to eq(nil) 40 | end 41 | end 42 | 43 | describe "from value to readable" do 44 | it "works with valid inputs" do 45 | expect(klass.from_itc_to_itc_readable('English_UK')).to eq('UK English') 46 | end 47 | 48 | it "returns nil when element doesn't exist" do 49 | expect(klass.from_itc_to_itc_readable('notHere')).to eq(nil) 50 | end 51 | end 52 | end 53 | 54 | describe String do 55 | describe "#to_language_code" do 56 | it "redirects to the actual converter" do 57 | expect("German".to_language_code).to eq("de-DE") 58 | end 59 | end 60 | 61 | describe "#to_full_language" do 62 | it "redirects to the actual converter" do 63 | expect("de".to_full_language).to eq("German") 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/language_converter.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | class LanguageConverter 4 | class << self 5 | # Converts the iTC format (English_CA, Brazilian Portuguese) to language short codes: (en-US, de-DE) 6 | def from_itc_to_standard(from) 7 | result = mapping.find { |a| a['name'] == from } 8 | (result || {}).fetch('locale', nil) 9 | end 10 | 11 | # Converts the language short codes: (en-US, de-DE) to the iTC format (English_CA, Brazilian Portuguese) 12 | def from_standard_to_itc(from) 13 | result = mapping.find { |a| a['locale'] == from || (a['alternatives'] || []).include?(from) } 14 | (result || {}).fetch('name', nil) 15 | end 16 | 17 | # Converts the langauge "UK English" (user facing) to "English_UK" (value) 18 | def from_itc_readable_to_itc(from) 19 | readable_mapping.each do |key, value| 20 | return key if value == from 21 | end 22 | nil 23 | end 24 | 25 | # Converts the langauge "English_UK" (value) to "UK English" (user facing) 26 | def from_itc_to_itc_readable(from) 27 | readable_mapping[from] 28 | end 29 | 30 | private 31 | 32 | # Path to the gem to fetch resoures 33 | def spaceship_gem_path 34 | if Gem::Specification.find_all_by_name('spaceship').any? 35 | return Gem::Specification.find_by_name('spaceship').gem_dir 36 | else 37 | return './' 38 | end 39 | end 40 | 41 | # Get the mapping JSON parsed 42 | def mapping 43 | @languages ||= JSON.parse(File.read(File.join(spaceship_gem_path, "lib", "assets", "languageMapping.json"))) 44 | end 45 | 46 | def readable_mapping 47 | @readable ||= JSON.parse(File.read(File.join(spaceship_gem_path, "lib", "assets", "languageMappingReadable.json"))) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | 54 | class String 55 | def to_language_code 56 | Spaceship::Tunes::LanguageConverter.from_itc_to_standard(self) 57 | end 58 | 59 | def to_full_language 60 | Spaceship::Tunes::LanguageConverter.from_standard_to_itc(self) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/portal/fixtures/submitCertificateRequest.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-29T16:07:12Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": "048120d0-d6eb-49b9-9c70-dff6ee4b3373", 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/certificate/submitCertificateRequest.action?content-type=text/x-url-arguments&accept=application/json&requestId=048120d0-d6eb-49b9-9c70-dff6ee4b3373&userLocale=en_US&teamId=S56PJTAYEL", 8 | "responseId": "a64fde92-de9e-46d2-b184-98af3d0a6c11", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": false, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "certRequest": { 16 | "certRequestId": "ESF52FQGU6", 17 | "statusCode": 4, 18 | "statusString": "Issued", 19 | "csrPlatform": "ios", 20 | "dateRequestedString": "Apr 29, 2015", 21 | "dateRequested": "2015-04-29T16:07:11Z", 22 | "expirationDate": "2016-04-28T15:57:11Z", 23 | "expirationDateString": "Apr 28, 2016", 24 | "certificateId": "HG3FQ9M282", 25 | "typeString": "APNs Production iOS", 26 | "certificateType": { 27 | "certificateTypeDisplayId": "UPV3DW712I", 28 | "name": "APNs Production iOS", 29 | "platform": "ios", 30 | "permissionType": "production", 31 | "distributionType": "production", 32 | "distributionMethod": "push", 33 | "ownerType": "bundle", 34 | "daysOverlap": 364, 35 | "maxActive": 2 36 | }, 37 | "certificate": { 38 | "name": null, 39 | "certificateId": "HG3FQ9M282", 40 | "serialNumber": "7C4B29A2943BACA0", 41 | "status": "Issued", 42 | "statusCode": 0, 43 | "expirationDate": "2016-04-28", 44 | "certificatePlatform": "ios", 45 | "certificateType": { 46 | "certificateTypeDisplayId": "UPV3DW712I", 47 | "name": "APNs Production iOS", 48 | "platform": "ios", 49 | "permissionType": "production", 50 | "distributionType": "production", 51 | "distributionMethod": "push", 52 | "ownerType": "bundle", 53 | "daysOverlap": 364, 54 | "maxActive": 2 55 | }, 56 | "hasAskKey": false 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /spec/tunes/app_submission_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::AppSubmission do 4 | before { Spaceship::Tunes.login } 5 | 6 | let(:client) { Spaceship::AppSubmission.client } 7 | let(:app) { Spaceship::Application.all.first } 8 | 9 | describe "successfully creates a new app submission" do 10 | it "generates a new app submission from iTunes Connect response" do 11 | itc_stub_app_submissions 12 | submission = app.create_submission 13 | 14 | expect(submission.application).to eq(app) 15 | expect(submission.version.version).to eq(app.edit_version.version) 16 | expect(submission.export_compliance_platform).to eq("ios") 17 | expect(submission.export_compliance_app_type).to eq("iOS App") 18 | expect(submission.export_compliance_compliance_required).to eq(true) 19 | end 20 | 21 | it "submits a valid app submission to iTunes Connect" do 22 | itc_stub_app_submissions 23 | submission = app.create_submission 24 | submission.content_rights_contains_third_party_content = true 25 | submission.content_rights_has_rights = true 26 | submission.add_id_info_uses_idfa = false 27 | submission.complete! 28 | 29 | expect(submission.submitted_for_review).to eq(true) 30 | end 31 | 32 | it "raises an error when submitting an app that has validation errors" do 33 | itc_stub_app_submissions_invalid 34 | 35 | expect do 36 | app.create_submission 37 | end.to raise_error "The App Name you entered has already been used. The App Name you entered has already been used. You must provide an address line. There are errors on the page and for 2 of your localizations." 38 | end 39 | 40 | it "raises an error when submitting an app that is already in review" do 41 | itc_stub_app_submissions_already_submitted 42 | submission = app.create_submission 43 | submission.content_rights_contains_third_party_content = true 44 | submission.content_rights_has_rights = true 45 | submission.add_id_info_uses_idfa = false 46 | 47 | expect do 48 | submission.complete! 49 | end.to raise_exception("Problem processing review submission.") 50 | expect(submission.submitted_for_review).to eq(false) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/app_status.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | # Defines the different states of the app 4 | # 5 | # As specified by Apple: https://developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnect_Guide/Chapters/ChangingAppStatus.html 6 | module AppStatus 7 | # You can edit this version, upload new binaries and more 8 | PREPARE_FOR_SUBMISSION = "Prepare for Submission" 9 | 10 | # App is currently live in the App Store 11 | READY_FOR_SALE = "Ready for Sale" 12 | 13 | # Waiting for Apple's Review 14 | WAITING_FOR_REVIEW = "Waiting For Review" 15 | 16 | # Currently in Review 17 | IN_REVIEW = "In Review" 18 | 19 | # App rejected for whatever reason 20 | REJECTED = "Rejected" 21 | 22 | # The developer took the app from the App Store 23 | DEVELOPER_REMOVED_FROM_SALE = "Developer Removed From Sale" 24 | 25 | # Developer rejected this version/binary 26 | DEVELOPER_REJECTED = "Developer Rejected" 27 | 28 | # You have to renew your Apple account to keep using iTunes Connect 29 | PENDING_CONTRACT = "Pending Contract" 30 | 31 | UPLOAD_RECEIVED = "Upload Received" 32 | PENDING_DEVELOPER_RELEASE = "Pending Developer Release" 33 | PROCESSING_FOR_APP_STORE = "Processing for App Store" 34 | 35 | # Unused app states 36 | # PENDING_APPLE_RELASE = "Pending Apple Release" 37 | 38 | # WAITING_FOR_EXPORT_COMPLIANCE = "Waiting For Export Compliance" 39 | # METADATA_REJECTED = "Metadata Rejected" 40 | # REMOVED_FROM_SALE = "Removed From Sale" 41 | # INVALID_BINARY = "Invalid Binary" 42 | 43 | # Get the app status matching based on a string (given by iTunes Connect) 44 | def self.get_from_string(text) 45 | mapping = { 46 | 'readyForSale' => READY_FOR_SALE, 47 | 'prepareForUpload' => PREPARE_FOR_SUBMISSION, 48 | 'devRejected' => DEVELOPER_REJECTED, 49 | 'pendingContract' => PENDING_CONTRACT, 50 | 'developerRemovedFromSale' => DEVELOPER_REMOVED_FROM_SALE, 51 | 'waitingForReview' => WAITING_FOR_REVIEW, 52 | 'inReview' => IN_REVIEW 53 | } 54 | 55 | mapping.each do |k, v| 56 | return v if k == text 57 | end 58 | 59 | return nil 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/create_application_prefill_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "companyName": { 7 | "value": "Felix Krause", 8 | "isEditable": false, 9 | "isRequired": false, 10 | "errorKeys": null 11 | }, 12 | "versionString": null, 13 | "appRegInfo": null, 14 | "bundleIds": { 15 | "*": "Xcode iOS Wildcard App ID - *" 16 | }, 17 | "enabledPlatformsForCreation": { 18 | "value": ["appletvos", "ios"], 19 | "isEditable": true, 20 | "isRequired": true, 21 | "errorKeys": null 22 | }, 23 | "name": null, 24 | "primaryLanguage": { 25 | "value": null, 26 | "isEditable": true, 27 | "isRequired": true, 28 | "errorKeys": null 29 | }, 30 | "bundleId": { 31 | "value": null, 32 | "isEditable": true, 33 | "isRequired": true, 34 | "errorKeys": null 35 | }, 36 | "bundleIdSuffix": { 37 | "value": null, 38 | "isEditable": true, 39 | "isRequired": true, 40 | "errorKeys": null 41 | }, 42 | "vendorId": null, 43 | "initialPlatform": "ios", 44 | "adamId": null, 45 | "newApp": { 46 | "sectionErrorKeys": [], 47 | "sectionInfoKeys": [], 48 | "sectionWarningKeys": [], 49 | "bundleId": { 50 | "value": null, 51 | "isEditable": true, 52 | "isRequired": true, 53 | "errorKeys": null 54 | }, 55 | "bundleIdSuffix": { 56 | "value": null, 57 | "isEditable": true, 58 | "isRequired": true, 59 | "errorKeys": null 60 | }, 61 | "vendorId": null, 62 | "adamId": null, 63 | "appType": "ios", 64 | "name": null, 65 | "liveVersion": null, 66 | "inFlightVersion": null, 67 | "primaryLanguage": { 68 | "value": null, 69 | "isEditable": true, 70 | "isRequired": true, 71 | "errorKeys": null 72 | }, 73 | "canAddVersion": false, 74 | "appPageSectionLinks": null, 75 | "appPageMoreLinks": null, 76 | "appPageActionLinks": null, 77 | "appTransferState": null, 78 | "isDeleted": false, 79 | "bundleSummaryInfo": null 80 | } 81 | }, 82 | "messages": { 83 | "warn": null, 84 | "error": null, 85 | "info": null 86 | }, 87 | "statusCode": "SUCCESS" 88 | } -------------------------------------------------------------------------------- /spec/portal/fixtures/getProvisioningProfile.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-05-14T15:02:01Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": "9d1eb364-dfa2-482b-9ff9-6bc3bb9c3d21", 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/profile/getProvisioningProfile.action?content-type=text/x-url-arguments&accept=application/json&requestId=9d1eb364-dfa2-482b-9ff9-6bc3bb9c3d21&userLocale=en_US&teamId=5A997XSHK2&includeInactiveProfiles=true&onlyCountLists=true", 8 | "responseId": "32ac3bfa-18e3-465f-9738-6721f87766ed", 9 | "isAdmin": false, 10 | "isMember": true, 11 | "isAgent": false, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "provisioningProfile": { 16 | "provisioningProfileId": "0123456789", 17 | "name": "1 Gut Altentann Ad Hoc", 18 | "status": "Invalid", 19 | "type": "iOS Distribution", 20 | "distributionMethod": "store", 21 | "proProPlatform": "ios", 22 | "version": "2", 23 | "dateExpire": "2015-11-25", 24 | "managingApp": null, 25 | "appId": { 26 | "appIdId": "2UMR2S6P4L", 27 | "name": "Gut Altentann", 28 | "appIdPlatform": "ios", 29 | "prefix": "5A997XSHK2", 30 | "identifier": "net.sunapps.1", 31 | "isWildCard": false, 32 | "isDuplicate": false, 33 | "features": { 34 | "push": true, 35 | "inAppPurchase": true, 36 | "gameCenter": true, 37 | "passbook": false, 38 | "dataProtection": "", 39 | "homeKit": false, 40 | "cloudKitVersion": 1, 41 | "iCloud": false, 42 | "LPLF93JG7M": false, 43 | "IAD53UNK2F": false, 44 | "V66P55NK2I": false, 45 | "SKC3T5S89Y": false, 46 | "APG3427HIY": false, 47 | "HK421J6T7P": false, 48 | "WC421J6T7P": false 49 | }, 50 | "enabledFeatures": [ 51 | "gameCenter", 52 | "inAppPurchase", 53 | "push" 54 | ], 55 | "isDevPushEnabled": false, 56 | "isProdPushEnabled": false, 57 | "associatedApplicationGroupsCount": null, 58 | "associatedCloudContainersCount": null, 59 | "associatedIdentifiersCount": null 60 | }, 61 | "appIdId": "2UMR2S6P4L", 62 | "deviceCount": 13, 63 | "certificateCount": 1, 64 | "UUID": "cc4d542e-1ee6-445f-967f-f19aea167e7a" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /spec/UI/select_team_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Client do 4 | describe "UI" do 5 | describe "#select_team" do 6 | subject { Spaceship.client } 7 | let(:username) { 'spaceship@krausefx.com' } 8 | let(:password) { 'so_secret' } 9 | 10 | before do 11 | Spaceship.login 12 | client = Spaceship.client 13 | end 14 | 15 | it "uses the first team if there is only one" do 16 | expect(subject.select_team).to eq("XXXXXXXXXX") 17 | end 18 | 19 | describe "Multiple Teams" do 20 | before do 21 | adp_stub_multiple_teams 22 | end 23 | 24 | it "Lets the user select the team if in multiple teams" do 25 | allow($stdin).to receive(:gets).and_return("2") 26 | expect(subject.select_team).to eq("SecondTeam") # a different team 27 | end 28 | 29 | it "Falls back to user selection if team wasn't found" do 30 | ENV["FASTLANE_TEAM_ID"] = "Not Here" 31 | allow($stdin).to receive(:gets).and_return("2") 32 | expect(subject.select_team).to eq("SecondTeam") # a different team 33 | end 34 | 35 | it "Uses the specific team (1/2)" do 36 | ENV["FASTLANE_TEAM_ID"] = "SecondTeam" 37 | expect(subject.select_team).to eq("SecondTeam") # a different team 38 | end 39 | 40 | it "Uses the specific team (2/2)" do 41 | ENV["FASTLANE_TEAM_ID"] = "XXXXXXXXXX" 42 | expect(subject.select_team).to eq("XXXXXXXXXX") # a different team 43 | end 44 | 45 | it "Let's the user specify the team name" do 46 | ENV["FASTLANE_TEAM_NAME"] = "SecondTeamProfiName" 47 | expect(subject.select_team).to eq("SecondTeam") 48 | end 49 | 50 | it "Strips out spaces before and after the team name" do 51 | ENV["FASTLANE_TEAM_NAME"] = " SecondTeamProfiName " 52 | expect(subject.select_team).to eq("SecondTeam") 53 | end 54 | 55 | it "Asks for the team if the name couldn't be found" do 56 | ENV["FASTLANE_TEAM_NAME"] = "NotExistent" 57 | allow($stdin).to receive(:gets).and_return("2") 58 | expect(subject.select_team).to eq("SecondTeam") 59 | end 60 | 61 | after do 62 | ENV.delete("FASTLANE_TEAM_ID") 63 | ENV.delete("FASTLANE_TEAM_NAME") 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/portal/fixtures/create_profile_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-07T09:53:37Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": "521b81f0-b2c4-47d6-a49d-6350e9658aaa", 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/profile/createProvisioningProfile.action?content-type=text/x-url-arguments&accept=application/json&requestId=521b81f0-b2c4-47d6-a49d-6350e965857b&userLocale=en_US&teamId=5A997XSHK2", 8 | "responseId": "dff94701-f0bb-471f-b764-0459b5d87aaa", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": true, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "provisioningProfile": { 16 | "provisioningProfileId": "W2MY88F6GE", 17 | "name": "asdfasdfasdf", 18 | "status": "Active", 19 | "type": "iOS Development", 20 | "distributionMethod": "limited", 21 | "proProPlatform": "ios", 22 | "version": "2", 23 | "dateExpire": "2016-04-06", 24 | "managingApp": null, 25 | "appId": { 26 | "appIdId": "RGAWZGXSY4", 27 | "name": "ABP", 28 | "appIdPlatform": "ios", 29 | "prefix": "5A997XSHK2", 30 | "identifier": "net.sunapps.34", 31 | "isWildCard": false, 32 | "isDuplicate": false, 33 | "features": { 34 | "push": true, 35 | "inAppPurchase": true, 36 | "gameCenter": true, 37 | "passbook": false, 38 | "dataProtection": "", 39 | "homeKit": false, 40 | "cloudKitVersion": 1, 41 | "iCloud": false, 42 | "LPLF93JG7M": false, 43 | "IAD53UNK2F": false, 44 | "V66P55NK2I": false, 45 | "SKC3T5S89Y": false, 46 | "APG3427HIY": false, 47 | "HK421J6T7P": false, 48 | "WC421J6T7P": false 49 | }, 50 | "enabledFeatures": ["gameCenter", "inAppPurchase", "push"], 51 | "isDevPushEnabled": false, 52 | "isProdPushEnabled": false, 53 | "associatedApplicationGroupsCount": null, 54 | "associatedCloudContainersCount": null, 55 | "associatedIdentifiersCount": null 56 | }, 57 | "appIdId": "RGAWZGXSY4", 58 | "deviceIds": ["RK3285QATH", "E687498679", "5YTNZ5A9RV", "VCD3RH54BK", "VA3Z744A8R", "T5VFWSCC2Z", "GD25LDGN99", "XJXGVS46MW", "LEL449RZER", "WXQ7V239BE", "9T5RA84V77", "S4227Y42V5", "L4378H292Z"], 59 | "certificateIds": ["C8DL7464RQ"], 60 | "encodedProfile": "MIIf...==", 61 | "filename": "asdfasdfasdf.mobileprovision", 62 | "UUID": "ad2281da-76c0-4698-80ac-c16c430e1a9a" 63 | } 64 | } -------------------------------------------------------------------------------- /spec/spaceship_base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Base do 4 | let(:client) { double('Client') } 5 | before { Spaceship::Portal.client = double('Default Client') } 6 | 7 | describe 'Class Methods' do 8 | it 'will use a default client' do 9 | expect(Spaceship::PortalBase.client).to eq(Spaceship::Portal.client) 10 | end 11 | 12 | it 'can set a client' do 13 | Spaceship::PortalBase.client = client 14 | expect(Spaceship::PortalBase.client).to eq(client) 15 | end 16 | 17 | it 'can set a client and return itself' do 18 | expect(Spaceship::PortalBase.set_client(client)).to eq(Spaceship::PortalBase) 19 | end 20 | 21 | describe 'instantiation from an attribute hash' do 22 | let(:test_class) do 23 | Class.new(Spaceship::PortalBase) do 24 | attr_accessor :some_attr_name 25 | attr_accessor :nested_attr_name 26 | attr_accessor :is_live 27 | 28 | attr_mapping({ 29 | 'someAttributeName' => :some_attr_name, 30 | 'nestedAttribute.name.value' => :nested_attr_name, 31 | 'isLiveString' => :is_live 32 | }) 33 | 34 | # rubocop:disable Style/PredicateName 35 | def is_live 36 | super == 'true' 37 | end 38 | # rubocop:enable Style/PredicateName 39 | end 40 | end 41 | 42 | it 'can create an attribute mapping' do 43 | inst = test_class.new('someAttributeName' => 'some value') 44 | expect(inst.some_attr_name).to eq('some value') 45 | end 46 | 47 | it 'can inherit the attribute mapping' do 48 | subclass = Class.new(test_class) 49 | inst = subclass.new('someAttributeName' => 'some value') 50 | expect(inst.some_attr_name).to eq('some value') 51 | end 52 | 53 | it 'can map nested attributes' do 54 | inst = test_class.new({ 'nestedAttribute' => { 'name' => { 'value' => 'a value' } } }) 55 | expect(inst.nested_attr_name).to eq('a value') 56 | end 57 | 58 | it 'can overwrite an attribute and call super' do 59 | inst = test_class.new({ 'isLiveString' => 'true' }) 60 | expect(inst.is_live).to eq(true) 61 | end 62 | end 63 | 64 | it 'can constantize subclasses by calling a method on the parent class' do 65 | class Developer < Spaceship::PortalBase 66 | class RubyDeveloper < Developer 67 | end 68 | end 69 | 70 | expect(Developer.ruby_developer).to eq(Developer::RubyDeveloper) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/spaceship/portal/app_group.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Portal 3 | # Represents an app group of the Apple Dev Portal 4 | class AppGroup < PortalBase 5 | # @return (String) The identifier assigned to this group 6 | # @example 7 | # "group.com.example.application" 8 | attr_accessor :group_id 9 | 10 | # @return (String) The prefix assigned to this group 11 | # @example 12 | # "9J57U9392R" 13 | attr_accessor :prefix 14 | 15 | # @return (String) The name of this group 16 | # @example 17 | # "App Group" 18 | attr_accessor :name 19 | 20 | # @return (String) Status of the group 21 | # @example 22 | # "current" 23 | attr_accessor :status 24 | 25 | # @return (String) The identifier of this app group, provided by the Dev Portal 26 | # @example 27 | # "2MAY7NPHAA" 28 | attr_accessor :app_group_id 29 | 30 | attr_mapping( 31 | 'applicationGroup' => :app_group_id, 32 | 'name' => :name, 33 | 'prefix' => :prefix, 34 | 'identifier' => :group_id, 35 | 'status' => :status 36 | ) 37 | 38 | class << self 39 | # Create a new object based on a hash. 40 | # This is used to create a new object based on the server response. 41 | def factory(attrs) 42 | self.new(attrs) 43 | end 44 | 45 | # @return (Array) Returns all app groups available for this account 46 | def all 47 | client.app_groups.map { |group| self.factory(group) } 48 | end 49 | 50 | # Creates a new App Group on the Apple Dev Portal 51 | # 52 | # @param group_id [String] the identifier to assign to this group 53 | # @param name [String] the name of the group 54 | # @return (AppGroup) The group you just created 55 | def create!(group_id: nil, name: nil) 56 | new_group = client.create_app_group!(name, group_id) 57 | self.new(new_group) 58 | end 59 | 60 | # Find a specific App Group group_id 61 | # @return (AppGroup) The app group you're looking for. This is nil if the app group can't be found. 62 | def find(group_id) 63 | all.find do |group| 64 | group.group_id == group_id 65 | end 66 | end 67 | end 68 | 69 | # Delete this app group 70 | # @return (AppGroup) The app group you just deletd 71 | def delete! 72 | client.delete_app_group!(app_group_id) 73 | self 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /.rubocop_general.yml: -------------------------------------------------------------------------------- 1 | 2 | Style/ClassCheck: 3 | EnforcedStyle: kind_of? 4 | 5 | # Cop supports --auto-correct. 6 | # Configuration parameters: EnforcedStyle, SupportedStyles. 7 | Style/BracesAroundHashParameters: 8 | Enabled: false 9 | 10 | Lint/UselessAssignment: 11 | Exclude: 12 | - 'spec/**/*' 13 | 14 | # Cop supports --auto-correct. 15 | # Configuration parameters: EnforcedStyle, SupportedStyles. 16 | Style/IndentHash: 17 | Enabled: false 18 | 19 | Style/RaiseArgs: 20 | EnforcedStyle: exploded 21 | 22 | Style/DoubleNegation: 23 | Enabled: false 24 | 25 | Lint/HandleExceptions: 26 | Enabled: false 27 | 28 | # Cop supports --auto-correct. 29 | Lint/UnusedBlockArgument: 30 | Enabled: false 31 | 32 | # Needed for $verbose 33 | Style/GlobalVars: 34 | Enabled: false 35 | 36 | Style/FileName: 37 | Enabled: false 38 | 39 | # $? Exit 40 | Style/SpecialGlobalVars: 41 | Enabled: false 42 | 43 | # the let(:key) { ... } should be allowed in tests 44 | Lint/ParenthesesAsGroupedExpression: 45 | Exclude: 46 | - 'spec/**/*' 47 | 48 | # options.rb might be large, we know that 49 | Metrics/MethodLength: 50 | Max: 60 51 | Exclude: 52 | - 'lib/*/options.rb' 53 | 54 | Metrics/AbcSize: 55 | Max: 60 56 | Exclude: 57 | - 'lib/*/options.rb' 58 | 59 | # Both string notations are okay 60 | Style/StringLiterals: 61 | Enabled: false 62 | 63 | # The %w might be confusing for new users 64 | Style/WordArray: 65 | MinSize: 19 66 | 67 | # Not a good thing 68 | Style/RedundantSelf: 69 | Enabled: false 70 | 71 | # raise and fail are both okay 72 | Style/SignalException: 73 | Enabled: false 74 | 75 | # Better too much 'return' than one missing 76 | Style/RedundantReturn: 77 | Enabled: false 78 | 79 | # Having if in the same line might not always be good 80 | Style/IfUnlessModifier: 81 | Enabled: false 82 | 83 | # That looks wrong 84 | Style/AlignHash: 85 | Enabled: false 86 | 87 | # and and or is okay 88 | Style/AndOr: 89 | Enabled: false 90 | 91 | # Configuration parameters: CountComments. 92 | Metrics/ClassLength: 93 | Max: 320 94 | 95 | Metrics/CyclomaticComplexity: 96 | Max: 17 97 | 98 | # Configuration parameters: AllowURI, URISchemes. 99 | Metrics/LineLength: 100 | Max: 370 101 | 102 | # Configuration parameters: CountKeywordArgs. 103 | Metrics/ParameterLists: 104 | Max: 17 105 | 106 | Metrics/PerceivedComplexity: 107 | Max: 18 108 | 109 | Style/DotPosition: 110 | Enabled: false 111 | 112 | Style/GuardClause: 113 | Enabled: false 114 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/create_application_prefill_first_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "companyName": { 7 | "value": null, 8 | "isEditable": false, 9 | "isRequired": true, 10 | "errorKeys": null 11 | }, 12 | "versionString": { 13 | "value": null, 14 | "isEditable": true, 15 | "isRequired": true, 16 | "errorKeys": null 17 | }, 18 | "appRegInfo": null, 19 | "bundleIds": { 20 | "com.krausefx.app_name": "Spaceship App - com.krausefx.app_name", 21 | "net.sunapps.*": "SunApps - net.sunapps.*", 22 | "net.sunapps.947474": "My App Name - net.sunapps.947474", 23 | "*": "Xcode iOS Wildcard App ID - *", 24 | "net.sunapps.100": "Baconfy - net.sunapps.100" 25 | }, 26 | "enabledPlatformsForCreation": { 27 | "value": ["ios"], 28 | "isEditable": true, 29 | "isRequired": true, 30 | "errorKeys": null 31 | }, 32 | "name": null, 33 | "primaryLanguage": null, 34 | "bundleId": null, 35 | "bundleIdSuffix": null, 36 | "vendorId": null, 37 | "initialPlatform": null, 38 | "adamId": null, 39 | "newApp": { 40 | "sectionErrorKeys": [], 41 | "sectionInfoKeys": [], 42 | "sectionWarningKeys": [], 43 | "bundleId": { 44 | "value": null, 45 | "isEditable": true, 46 | "isRequired": true, 47 | "errorKeys": null 48 | }, 49 | "bundleIdSuffix": { 50 | "value": null, 51 | "isEditable": true, 52 | "isRequired": false, 53 | "errorKeys": null 54 | }, 55 | "vendorId": { 56 | "value": null, 57 | "isEditable": true, 58 | "isRequired": true, 59 | "errorKeys": null 60 | }, 61 | "adamId": null, 62 | "appType": "iOS App", 63 | "name": { 64 | "value": null, 65 | "isEditable": true, 66 | "isRequired": true, 67 | "errorKeys": null 68 | }, 69 | "liveVersion": null, 70 | "inFlightVersion": null, 71 | "primaryLanguage": { 72 | "value": null, 73 | "isEditable": true, 74 | "isRequired": true, 75 | "errorKeys": null 76 | }, 77 | "canAddVersion": false, 78 | "appPageSectionLinks": null, 79 | "appPageMoreLinks": null, 80 | "appPageActionLinks": null, 81 | "appTransferState": null, 82 | "isDeleted": false, 83 | "bundleSummaryInfo": null 84 | } 85 | }, 86 | "messages": { 87 | "warn": null, 88 | "error": null, 89 | "info": null 90 | }, 91 | "statusCode": "SUCCESS" 92 | } -------------------------------------------------------------------------------- /spec/tunes/fixtures/create_application_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "companyName": { 7 | "value": "SunApps GmbH", 8 | "isEditable": false, 9 | "isRequired": false, 10 | "errorKeys": null 11 | }, 12 | "versionString": { 13 | "value": null, 14 | "isEditable": true, 15 | "isRequired": true, 16 | "errorKeys": null 17 | }, 18 | "appRegInfo": null, 19 | "bundleIds": { 20 | "com.krausefx.app_name": "Spaceship App - com.krausefx.app_name", 21 | "net.sunapps.*": "SunApps - net.sunapps.*", 22 | "net.sunapps.947474": "My App Name - net.sunapps.947474", 23 | "*": "Xcode iOS Wildcard App ID - *", 24 | "net.sunapps.100": "Baconfy - net.sunapps.100" 25 | }, 26 | "enabledPlatformsForCreation": { 27 | "value": ["ios"], 28 | "isEditable": true, 29 | "isRequired": true, 30 | "errorKeys": null 31 | }, 32 | "name": null, 33 | "primaryLanguage": null, 34 | "bundleId": null, 35 | "bundleIdSuffix": null, 36 | "vendorId": null, 37 | "initialPlatform": null, 38 | "adamId": null, 39 | "newApp": { 40 | "sectionErrorKeys": [], 41 | "sectionInfoKeys": [], 42 | "sectionWarningKeys": [], 43 | "bundleId": { 44 | "value": null, 45 | "isEditable": true, 46 | "isRequired": true, 47 | "errorKeys": null 48 | }, 49 | "bundleIdSuffix": { 50 | "value": null, 51 | "isEditable": true, 52 | "isRequired": false, 53 | "errorKeys": null 54 | }, 55 | "vendorId": { 56 | "value": null, 57 | "isEditable": true, 58 | "isRequired": true, 59 | "errorKeys": null 60 | }, 61 | "adamId": null, 62 | "appType": "iOS App", 63 | "name": { 64 | "value": null, 65 | "isEditable": true, 66 | "isRequired": true, 67 | "errorKeys": null 68 | }, 69 | "liveVersion": null, 70 | "inFlightVersion": null, 71 | "primaryLanguage": { 72 | "value": null, 73 | "isEditable": true, 74 | "isRequired": true, 75 | "errorKeys": null 76 | }, 77 | "canAddVersion": false, 78 | "appPageSectionLinks": null, 79 | "appPageMoreLinks": null, 80 | "appPageActionLinks": null, 81 | "appTransferState": null, 82 | "isDeleted": false, 83 | "bundleSummaryInfo": null 84 | } 85 | }, 86 | "messages": { 87 | "warn": null, 88 | "error": null, 89 | "info": null 90 | }, 91 | "statusCode": "SUCCESS" 92 | } -------------------------------------------------------------------------------- /spec/du/du_client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::DUClient, :du do 4 | before { Spaceship::Tunes.login } 5 | 6 | let(:client) { Spaceship::AppVersion.client } 7 | let(:app) { Spaceship::Application.all.first } 8 | let(:contentProviderId) { "1234567" } 9 | let(:ssoTokenForImage) { "sso token for image" } 10 | let(:version) { app.edit_version } 11 | 12 | subject { Spaceship::DUClient.new } 13 | 14 | describe "#upload_large_icon and #upload_watch_icon" do 15 | let(:correct_jpg_image) { du_uploadimage_correct_jpg } 16 | let(:bad_png_image) { du_uploadimage_invalid_png } 17 | 18 | it "handles successful upload request using #upload_large_icon" do 19 | du_upload_large_image_success 20 | 21 | data = subject.upload_large_icon(version, correct_jpg_image, contentProviderId, ssoTokenForImage) 22 | expect(data['token']).to eq('Purple7/v4/65/04/4d/65044dae-15b0-a5e0-d021-5aa4162a03a3/pr_source.jpg') 23 | end 24 | 25 | it "handles failed upload request using #upload_watch_icon" do 26 | du_upload_watch_image_failure 27 | 28 | error_text = "[IMG_ALPHA_NOT_ALLOWED] Alpha is not allowed. Please edit the image to remove alpha and re-save it." 29 | expect do 30 | data = subject.upload_watch_icon(version, bad_png_image, contentProviderId, ssoTokenForImage) 31 | end.to raise_error(Spaceship::Client::UnexpectedResponse, error_text) 32 | end 33 | end 34 | 35 | describe "#upload_geojson" do 36 | let(:valid_geojson) { du_upload_valid_geojson } 37 | let(:invalid_geojson) { du_upload_invalid_geojson } 38 | 39 | it "handles successful upload request" do 40 | du_upload_geojson_success 41 | 42 | data = subject.upload_geojson(version, valid_geojson, contentProviderId, ssoTokenForImage) 43 | expect(data['token']).to eq('Purple1/v4/45/50/9d/45509d39-6a5d-7f55-f919-0fbc7436be61/pr_source.geojson') 44 | expect(data['dsId']).to eq(1_206_675_732) 45 | expect(data['type']).to eq('SMGameCenterAvatarImageType.SOURCE') 46 | end 47 | 48 | it "handles failed upload request" do 49 | du_upload_geojson_failure 50 | 51 | error_text = "[FILE_GEOJSON_FIELD_NOT_SUPPORTED] The routing app coverage file is in the wrong format. For more information, see the Location and Maps Programming Guide in the iOS Developer Library." 52 | expect do 53 | data = subject.upload_geojson(version, invalid_geojson, contentProviderId, ssoTokenForImage) 54 | end.to raise_error(Spaceship::Client::UnexpectedResponse, error_text) 55 | end 56 | end 57 | 58 | describe "#upload_screenshot" do 59 | # we voluntary skip tests for this method, it's mostly covered by other functions 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/create_application_broken.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": ["You must choose a primary language."], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "companyName": { 7 | "value": "SunApps GmbH", 8 | "isEditable": false, 9 | "isRequired": false, 10 | "errorKeys": null 11 | }, 12 | "versionString": { 13 | "value": "1.0", 14 | "isEditable": true, 15 | "isRequired": true, 16 | "errorKeys": [] 17 | }, 18 | "appRegInfo": null, 19 | "bundleIds": { 20 | "com.krausefx.app_name": "Spaceship App - com.krausefx.app_name", 21 | "net.sunapps.*": "SunApps - net.sunapps.*", 22 | "net.sunapps.947474": "My App Name - net.sunapps.947474", 23 | "*": "Xcode iOS Wildcard App ID - *", 24 | "net.sunapps.100": "Baconfy - net.sunapps.100" 25 | }, 26 | "enabledPlatformsForCreation": { 27 | "value": ["ios"], 28 | "isEditable": true, 29 | "isRequired": true, 30 | "errorKeys": null 31 | }, 32 | "name": null, 33 | "primaryLanguage": null, 34 | "bundleId": null, 35 | "bundleIdSuffix": null, 36 | "vendorId": null, 37 | "initialPlatform": null, 38 | "adamId": null, 39 | "newApp": { 40 | "sectionErrorKeys": [], 41 | "sectionInfoKeys": [], 42 | "sectionWarningKeys": [], 43 | "bundleId": { 44 | "value": "net.sunapps.123", 45 | "isEditable": true, 46 | "isRequired": true, 47 | "errorKeys": [] 48 | }, 49 | "bundleIdSuffix": { 50 | "value": null, 51 | "isEditable": true, 52 | "isRequired": false, 53 | "errorKeys": null 54 | }, 55 | "vendorId": { 56 | "value": "SKU123", 57 | "isEditable": true, 58 | "isRequired": true, 59 | "errorKeys": [] 60 | }, 61 | "adamId": null, 62 | "appType": "iOS App", 63 | "name": { 64 | "value": "My Name", 65 | "isEditable": true, 66 | "isRequired": true, 67 | "errorKeys": [] 68 | }, 69 | "liveVersion": null, 70 | "inFlightVersion": null, 71 | "primaryLanguage": { 72 | "value": "Such Primary", 73 | "isEditable": true, 74 | "isRequired": true, 75 | "errorKeys": ["You must choose a primary language."] 76 | }, 77 | "canAddVersion": false, 78 | "appPageSectionLinks": null, 79 | "appPageMoreLinks": null, 80 | "appPageActionLinks": null, 81 | "appTransferState": null, 82 | "isDeleted": false, 83 | "bundleSummaryInfo": null 84 | } 85 | }, 86 | "messages": { 87 | "warn": null, 88 | "error": null, 89 | "info": ["Validation Error(s)."] 90 | }, 91 | "statusCode": "SUCCESS" 92 | } -------------------------------------------------------------------------------- /spec/tunes/fixtures/create_application_wildcard_broken.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": ["You must enter a Bundle ID Suffix."], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "companyName": { 7 | "value": "SunApps GmbH", 8 | "isEditable": false, 9 | "isRequired": false, 10 | "errorKeys": null 11 | }, 12 | "versionString": { 13 | "value": "1.0", 14 | "isEditable": true, 15 | "isRequired": true, 16 | "errorKeys": [] 17 | }, 18 | "appRegInfo": null, 19 | "bundleIds": { 20 | "com.krausefx.app_name": "Spaceship App - com.krausefx.app_name", 21 | "net.sunapps.*": "SunApps - net.sunapps.*", 22 | "net.sunapps.947474": "My App Name - net.sunapps.947474", 23 | "*": "Xcode iOS Wildcard App ID - *", 24 | "net.sunapps.100": "Baconfy - net.sunapps.100" 25 | }, 26 | "enabledPlatformsForCreation": { 27 | "value": ["ios"], 28 | "isEditable": true, 29 | "isRequired": true, 30 | "errorKeys": null 31 | }, 32 | "name": null, 33 | "primaryLanguage": null, 34 | "bundleId": null, 35 | "bundleIdSuffix": null, 36 | "vendorId": null, 37 | "initialPlatform": null, 38 | "adamId": null, 39 | "newApp": { 40 | "sectionErrorKeys": [], 41 | "sectionInfoKeys": [], 42 | "sectionWarningKeys": [], 43 | "bundleId": { 44 | "value": "net.sunapps.123", 45 | "isEditable": true, 46 | "isRequired": true, 47 | "errorKeys": [] 48 | }, 49 | "bundleIdSuffix": { 50 | "value": null, 51 | "isEditable": true, 52 | "isRequired": false, 53 | "errorKeys": null 54 | }, 55 | "vendorId": { 56 | "value": "SKU123", 57 | "isEditable": true, 58 | "isRequired": true, 59 | "errorKeys": [] 60 | }, 61 | "adamId": null, 62 | "appType": "iOS App", 63 | "name": { 64 | "value": "My Name", 65 | "isEditable": true, 66 | "isRequired": true, 67 | "errorKeys": [] 68 | }, 69 | "liveVersion": null, 70 | "inFlightVersion": null, 71 | "primaryLanguage": { 72 | "value": "Such Primary", 73 | "isEditable": true, 74 | "isRequired": true, 75 | "errorKeys": ["You must enter a Bundle ID Suffix."] 76 | }, 77 | "canAddVersion": false, 78 | "appPageSectionLinks": null, 79 | "appPageMoreLinks": null, 80 | "appPageActionLinks": null, 81 | "appTransferState": null, 82 | "isDeleted": false, 83 | "bundleSummaryInfo": null 84 | } 85 | }, 86 | "messages": { 87 | "warn": null, 88 | "error": null, 89 | "info": ["Validation Error(s)."] 90 | }, 91 | "statusCode": "SUCCESS" 92 | } -------------------------------------------------------------------------------- /spec/tunes/fixtures/create_application_first_broken.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": ["You must provide a company name to use on the App Store."], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "companyName": { 7 | "value": null, 8 | "isEditable": true, 9 | "isRequired": true, 10 | "errorKeys":[ 11 | "You must provide a company name to use on the App Store." 12 | ] 13 | }, 14 | "versionString": { 15 | "value": "1.0", 16 | "isEditable": true, 17 | "isRequired": true, 18 | "errorKeys": [] 19 | }, 20 | "appRegInfo": null, 21 | "bundleIds": { 22 | "com.krausefx.app_name": "Spaceship App - com.krausefx.app_name", 23 | "net.sunapps.*": "SunApps - net.sunapps.*", 24 | "net.sunapps.947474": "My App Name - net.sunapps.947474", 25 | "*": "Xcode iOS Wildcard App ID - *", 26 | "net.sunapps.100": "Baconfy - net.sunapps.100" 27 | }, 28 | "enabledPlatformsForCreation": { 29 | "value": ["ios"], 30 | "isEditable": true, 31 | "isRequired": true, 32 | "errorKeys": null 33 | }, 34 | "name": null, 35 | "primaryLanguage": null, 36 | "bundleId": null, 37 | "bundleIdSuffix": null, 38 | "vendorId": null, 39 | "initialPlatform": null, 40 | "adamId": null, 41 | "newApp": { 42 | "sectionErrorKeys": [], 43 | "sectionInfoKeys": [], 44 | "sectionWarningKeys": [], 45 | "bundleId": { 46 | "value": "net.sunapps.123", 47 | "isEditable": true, 48 | "isRequired": true, 49 | "errorKeys": [] 50 | }, 51 | "bundleIdSuffix": { 52 | "value": null, 53 | "isEditable": true, 54 | "isRequired": false, 55 | "errorKeys": null 56 | }, 57 | "vendorId": { 58 | "value": "SKU123", 59 | "isEditable": true, 60 | "isRequired": true, 61 | "errorKeys": [] 62 | }, 63 | "adamId": null, 64 | "appType": "iOS App", 65 | "name": { 66 | "value": "My Name", 67 | "isEditable": true, 68 | "isRequired": true, 69 | "errorKeys": [] 70 | }, 71 | "liveVersion": null, 72 | "inFlightVersion": null, 73 | "primaryLanguage": { 74 | "value": "Such Primary", 75 | "isEditable": true, 76 | "isRequired": true, 77 | "errorKeys": [] 78 | }, 79 | "canAddVersion": false, 80 | "appPageSectionLinks": null, 81 | "appPageMoreLinks": null, 82 | "appPageActionLinks": null, 83 | "appTransferState": null, 84 | "isDeleted": false, 85 | "bundleSummaryInfo": null 86 | } 87 | }, 88 | "messages": { 89 | "warn": null, 90 | "error": null, 91 | "info": ["Validation Error(s)."] 92 | }, 93 | "statusCode": "SUCCESS" 94 | } -------------------------------------------------------------------------------- /spec/tunes/fixtures/app_resolution_center.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "replyConstraints": { 7 | "minLength": 1, 8 | "maxLength": 4000 9 | }, 10 | "appNotes": { 11 | "threads": [{ 12 | "id": "206695", 13 | "versionId": "812649", 14 | "version": "0.9.13", 15 | "messages": [{ 16 | "from": "Apple", 17 | "date": 1435181415000, 18 | "body": "2.16 Details

Your app declares support for audio in the UIBackgroundModes key in your Info.plist, but we were unable to play any audible content when the application was running in the background.

Next Steps

The audio key is intended for use by applications that provide audible content to the user while in the background, such as music player or streaming audio applications. Please revise your app to provide audible content to the user while the app is in the background or remove the \"audio\" setting from the UIBackgroundModes key.

Resources

If you have difficulty reproducing a reported issue, please try testing the workflow described in Technical Q&A QA1764: How to reproduce bugs reported against App Store submissions.

If you have code-level questions after utilizing the above resources, you may wish to consult with Apple Developer Technical Support. When the DTS engineer follows up with you, please be ready to provide:
- complete details of your rejection issue(s)
- screenshots
- steps to reproduce the issue(s)
- symbolicated crash logs - if your issue results in a crash log

", 19 | "appleMsg": true, 20 | "tokens": [], 21 | "hasObjectionableContent": false, 22 | "qcRejectionReasons": [{ 23 | "section": "2.16", 24 | "description": " Multitasking Apps may only use background services for their intended purposes: VoIP, audio playback, location, task completion, local notifications, etc." 25 | }] 26 | }], 27 | "active": true, 28 | "canDeveloperAddNote": true, 29 | "metadataRejected": false, 30 | "binaryRejected": true 31 | }] 32 | }, 33 | "betaNotes": { 34 | "threads": [] 35 | }, 36 | "appMessages": { 37 | "threads": [] 38 | } 39 | }, 40 | "messages": { 41 | "warn": null, 42 | "error": null, 43 | "info": null 44 | }, 45 | "statusCode": "SUCCESS" 46 | } -------------------------------------------------------------------------------- /spec/tunes/fixtures/testers/get_external.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "testers": [{ 7 | "emailAddress": { 8 | "value": "private@krausefx.com", 9 | "isEditable": false, 10 | "isRequired": false, 11 | "errorKeys": null, 12 | "maxLength": 2147483647, 13 | "minLength": 0 14 | }, 15 | "firstName": { 16 | "value": "Detlef", 17 | "isEditable": false, 18 | "isRequired": false, 19 | "errorKeys": null, 20 | "maxLength": 2147483647, 21 | "minLength": 0 22 | }, 23 | "lastName": { 24 | "value": "Müller", 25 | "isEditable": false, 26 | "isRequired": false, 27 | "errorKeys": null, 28 | "maxLength": 2147483647, 29 | "minLength": 0 30 | }, 31 | "testerId": "1d167b89-13c5-4dd8-b988-7a6a0190faaa", 32 | "devices": [{ 33 | "model": "iPhone 6", 34 | "os": "iOS", 35 | "osVersion": "8.3", 36 | "name": null 37 | }], 38 | "latestInstalledAppAdamId": 794902327, 39 | "latestInstalledVersion": "0.9.14", 40 | "latestInstalledShortVersion": "1", 41 | "latestInstalledDate": 1427565638420, 42 | "latestInstalledName": "App Name Yeah", 43 | "latestInstallByPlatform": null, 44 | "hasAddedOrInvitedApp": null, 45 | "warnOnDelete": true, 46 | "itcUsername": null, 47 | "groups": [] 48 | }, { 49 | "emailAddress": { 50 | "value": "ich@felixkrause.at", 51 | "isEditable": false, 52 | "isRequired": false, 53 | "errorKeys": null, 54 | "maxLength": 2147483647, 55 | "minLength": 0 56 | }, 57 | "firstName": { 58 | "value": "Felix", 59 | "isEditable": false, 60 | "isRequired": false, 61 | "errorKeys": null, 62 | "maxLength": 2147483647, 63 | "minLength": 0 64 | }, 65 | "lastName": { 66 | "value": "Krause", 67 | "isEditable": false, 68 | "isRequired": false, 69 | "errorKeys": null, 70 | "maxLength": 2147483647, 71 | "minLength": 0 72 | }, 73 | "testerId": "1399e231-27bd-496d-83f5-2f067e923140", 74 | "devices": [], 75 | "latestInstalledAppAdamId": null, 76 | "latestInstalledVersion": null, 77 | "latestInstalledShortVersion": null, 78 | "latestInstalledDate": null, 79 | "latestInstalledName": null, 80 | "latestInstallByPlatform": null, 81 | "hasAddedOrInvitedApp": null, 82 | "warnOnDelete": true, 83 | "itcUsername": null, 84 | "groups": [] 85 | }], 86 | "devices": { 87 | "iPhone": ["iPhone 6"] 88 | }, 89 | "groups": { 90 | "8dfd9f94-3983-46d5-a298-c866b2369a33": "MyGroup" 91 | } 92 | }, 93 | "messages": { 94 | "warn": null, 95 | "error": null, 96 | "info": null 97 | }, 98 | "statusCode": "SUCCESS" 99 | } -------------------------------------------------------------------------------- /spec/tunes/build_train_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Tunes::BuildTrain do 4 | before { Spaceship::Tunes.login } 5 | subject { Spaceship::Tunes.client } 6 | let(:username) { 'spaceship@krausefx.com' } 7 | let(:password) { 'so_secret' } 8 | 9 | describe "properly parses the train" do 10 | let(:app) { Spaceship::Application.all.first } 11 | 12 | it "inspect works" do 13 | expect(Spaceship::Application.all.first.build_trains.values.first.inspect).to include("Tunes::BuildTrain") 14 | end 15 | 16 | it "works filled in all required values" do 17 | trains = app.build_trains 18 | 19 | expect(trains.count).to eq(2) 20 | train = trains.values.first 21 | 22 | expect(train.version_string).to eq("1.0") 23 | expect(train.platform).to eq("ios") 24 | expect(train.application).to eq(app) 25 | 26 | # TestFlight 27 | expect(trains.values.first.external_testing_enabled).to eq(false) 28 | expect(trains.values.first.internal_testing_enabled).to eq(true) 29 | expect(trains.values.last.external_testing_enabled).to eq(false) 30 | expect(trains.values.last.internal_testing_enabled).to eq(false) 31 | end 32 | 33 | it "returns all processing builds" do 34 | builds = app.all_processing_builds 35 | expect(builds.count).to eq(3) 36 | end 37 | 38 | describe "Accessing builds" do 39 | it "lets the user fetch the builds for a given train" do 40 | train = app.build_trains.values.first 41 | expect(train.builds.count).to eq(1) 42 | end 43 | 44 | it "lets the user fetch the builds using the version as a key" do 45 | train = app.build_trains['1.0'] 46 | expect(train.version_string).to eq('1.0') 47 | expect(train.platform).to eq('ios') 48 | expect(train.internal_testing_enabled).to eq(true) 49 | expect(train.external_testing_enabled).to eq(false) 50 | expect(train.builds.count).to eq(1) 51 | end 52 | end 53 | 54 | describe "Processing builds" do 55 | it "builds that are stuck or pre-processing" do 56 | expect(app.invalid_builds.count).to eq(1) 57 | 58 | created_and_stucked = app.invalid_builds.first 59 | expect(created_and_stucked.upload_date).to eq(1_436_381_720_000) 60 | expect(created_and_stucked.state).to eq("invalidBinary") 61 | end 62 | 63 | it "properly extracted the processing builds from a train" do 64 | train = app.build_trains['1.0'] 65 | expect(train.processing_builds.count).to eq(0) 66 | end 67 | end 68 | 69 | describe "#update_testing_status" do 70 | it "just works (tm)" do 71 | train1 = app.build_trains['1.0'] 72 | train2 = app.build_trains['1.1'] 73 | expect(train1.internal_testing_enabled).to eq(true) 74 | expect(train2.internal_testing_enabled).to eq(false) 75 | 76 | train2.update_testing_status!(true, 'internal') 77 | 78 | expect(train2.internal_testing_enabled).to eq(true) 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/portal/fixtures/repair_profile_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-05-23T20:39:54Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": "c27b0eb3-17bf-4c21-a627-ea433314eaaa", 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/profile/regenProvisioningProfile.action?content-type=text/x-url-arguments&accept=application/json&requestId=c27b0eb3-17bf-4c21-aaaa-ea433314e708&userLocale=en_US&teamId=5A997XSAAA", 8 | "responseId": "0fdbb0ca-5dec-4ab8-bed4-93ac33e31aaa", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": true, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "httpResponseHeaders": null, 16 | "provisioningProfile": { 17 | "provisioningProfileId": "68M4242AAA", 18 | "name": "Le Profile Name", 19 | "status": "Active", 20 | "type": "iOS Distribution", 21 | "distributionMethod": "store", 22 | "proProPlatform": "ios", 23 | "version": "2", 24 | "dateExpire": "2015-11-25", 25 | "managingApp": null, 26 | "appId": { 27 | "appIdId": "2UMR2SAAA", 28 | "name": "App Name", 29 | "appIdPlatform": "ios", 30 | "prefix": "5A997XSAAA", 31 | "identifier": "net.sunapps.1", 32 | "isWildCard": false, 33 | "isDuplicate": false, 34 | "features": { 35 | "push": true, 36 | "inAppPurchase": true, 37 | "gameCenter": true, 38 | "passbook": false, 39 | "dataProtection": "", 40 | "homeKit": false, 41 | "cloudKitVersion": 1, 42 | "iCloud": false, 43 | "LPLF93JG7M": false, 44 | "IAD53UNK2F": false, 45 | "V66P55NK2I": false, 46 | "SKC3T5S89Y": false, 47 | "APG3427HIY": false, 48 | "HK421J6T7P": false, 49 | "WC421J6T7P": false 50 | }, 51 | "enabledFeatures": ["gameCenter", "inAppPurchase", "push"], 52 | "isDevPushEnabled": false, 53 | "isProdPushEnabled": false, 54 | "associatedApplicationGroupsCount": null, 55 | "associatedCloudContainersCount": null, 56 | "associatedIdentifiersCount": null 57 | }, 58 | "appIdId": "2UMR2S6P4L", 59 | "deviceIds": ["5YTNZ5A9RV", "GD25LDGN99", "XJXGVS46MW", "LEL449RZER", "WXQ7V239BE", "9T5RA84V77", "S4227Y42V5", "L4378H292Z"], 60 | "certificateIds": ["XC5PH8D47H"], 61 | "encodedProfile": "MIIesgYJKoZIhvcNAQcCoIIeozCCHp8CAQExCzAJBgUrDgMCGgUAMIIOeAYJKoZIhvcNAQcBoIIOaQSCDmU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCI/Pgo8IURPQ1RZUEUgcGxpc3QgUFVCTElDICItLy9BcHBsZS8vRFREIFBMSVNUIDEuMC8vRU4iICJodHRwOi8vd3d3LmFwcGxlLmNvbS9EVERzL1Byb3BlcnR5TGlzdC0xLjAuZHRkIj4KPHBsaXN0IHZlcnNpb249IjEuMCI+CjxkaWN0PgoJPGtleT5BcHBJRE5hbWU8L2tleT4KCTxzdHJpbmc+R3V0IEFsdGVudGFubjwvc3RyaW5nPgoJPGtleT5BcHBsaWNhdGlvbklkZW50aWZpZXJQcmVmaXg8L2tleT4KCTxhcnJheT4KCTxzdHJpbmc+NUE5OTdYU0hLMjwvc3RyaW5nPgoJPC9hcnJheT4KCTxrZXk+Q3JlYXRpb25EYXRlPC9rZXk+Cgk8ZGF0ZT4yMDE1LTA1LTIzVDIwOjM5OjU0WjwvZGF0ZT4KCTxrZXk+", 62 | "filename": "some_random_file_name.mobileprovision", 63 | "UUID": "5ac4ed6b-3f86-4843-82ea-1e7e968c717e" 64 | } 65 | } -------------------------------------------------------------------------------- /spec/tunes/testers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Tunes::Tester do 4 | before { Spaceship::Tunes.login } 5 | 6 | let(:client) { Spaceship::AppSubmission.client } 7 | let(:app) { Spaceship::Application.all.first } 8 | 9 | it "raises an error when using the base class" do 10 | expect do 11 | Spaceship::Tunes::Tester.all 12 | end.to raise_error "You have to use a subclass: Internal or External" 13 | end 14 | 15 | describe "Receiving existing testers" do 16 | it "inspect works, by also fetching the parent's attributes" do 17 | t = Spaceship::Tunes::Tester::Internal.all.first 18 | expect(t.inspect).to include("Tunes::Tester") 19 | expect(t.inspect).to include("latest_install_app_id=") 20 | end 21 | 22 | it "Internal Testers" do 23 | testers = Spaceship::Tunes::Tester::Internal.all 24 | expect(testers.count).to eq(2) 25 | t = testers[1] 26 | expect(t.class).to eq(Spaceship::Tunes::Tester::Internal) 27 | 28 | expect(t.tester_id).to eq("1d167b89-13c5-4dd8-b988-7a6a0190f774") 29 | expect(t.email).to eq("felix@sunapps.net") 30 | expect(t.first_name).to eq("Felix") 31 | expect(t.last_name).to eq("Krause") 32 | expect(t.devices).to eq([{ "model" => "iPhone 6", "os" => "iOS", "osVersion" => "8.3", "name" => nil }]) 33 | end 34 | 35 | it "External Testers" do 36 | testers = Spaceship::Tunes::Tester::External.all 37 | expect(testers.count).to eq(2) 38 | t = testers[0] 39 | expect(t.class).to eq(Spaceship::Tunes::Tester::External) 40 | 41 | expect(t.tester_id).to eq("1d167b89-13c5-4dd8-b988-7a6a0190faaa") 42 | expect(t.email).to eq("private@krausefx.com") 43 | expect(t.first_name).to eq("Detlef") 44 | expect(t.last_name).to eq("Müller") 45 | expect(t.devices).to eq([{ "model" => "iPhone 6", "os" => "iOS", "osVersion" => "8.3", "name" => nil }]) 46 | end 47 | end 48 | 49 | describe "Receiving existing testers from an app" do 50 | it "Internal Testers" do 51 | testers = app.internal_testers 52 | expect(testers.count).to eq(1) 53 | t = testers.first 54 | expect(t.class).to eq(Spaceship::Tunes::Tester::Internal) 55 | 56 | expect(t.tester_id).to eq("1d167b89-13c5-4dd8-b988-7a6a0190f774") 57 | expect(t.email).to eq("felix@sunapps.net") 58 | expect(t.first_name).to eq("Felix") 59 | expect(t.last_name).to eq("Krause") 60 | expect(t.devices).to eq([]) 61 | end 62 | end 63 | 64 | describe "Last Install information" do 65 | it "pre-fills this information correctly" do 66 | tester = Spaceship::Tunes::Tester::Internal.all[1] 67 | expect(tester.latest_install_app_id).to eq(794_902_327) 68 | expect(tester.latest_install_date).to eq(1_427_565_638_420) 69 | expect(tester.latest_installed_build_number).to eq("1") 70 | expect(tester.latest_installed_version_number).to eq("0.9.14") 71 | end 72 | end 73 | 74 | # describe "invite testers to an existing app" do 75 | # it "invite all users to an app" do 76 | # app.add_all_testers! 77 | # end 78 | # end 79 | end 80 | -------------------------------------------------------------------------------- /lib/spaceship/portal/ui/select_team.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | class Client 3 | class UserInterface 4 | # Shows the UI to select a team 5 | # @example teams value: 6 | # [{"status"=>"active", 7 | # "teamId"=>"5A997XAAAA", 8 | # "type"=>"Company/Organization", 9 | # "extendedTeamAttributes"=>{}, 10 | # "teamAgent"=>{ 11 | # "personId"=>15534241111, 12 | # "firstName"=>"Felix", 13 | # "lastName"=>"Krause", 14 | # "email"=>"spaceship@krausefx.com", 15 | # "developerStatus"=>"active", 16 | # "teamMemberId"=>"5Y354CXAAA"}, 17 | # "memberships"=> 18 | # [{"membershipId"=>"HJ5WHYC5CE", 19 | # "membershipProductId"=>"ds1", 20 | # "status"=>"active", 21 | # "inDeviceResetWindow"=>false, 22 | # "inRenewalWindow"=>false, 23 | # "dateStart"=>"11/20/14 07:59", 24 | # "dateExpire"=>"11/20/15 07:59", 25 | # "platform"=>"ios", 26 | # "availableDeviceSlots"=>100, 27 | # "name"=>"iOS Developer Program"}], 28 | # "currentTeamMember"=> 29 | # {"personId"=>nil, "firstName"=>nil, "lastName"=>nil, "email"=>nil, "developerStatus"=>nil, "privileges"=>{}, "roles"=>["TEAM_ADMIN"], "teamMemberId"=>"HQR8N4GAAA"}, 30 | # "name"=>"Company GmbH"}, 31 | # {...} 32 | # ] 33 | 34 | def select_team 35 | teams = client.teams 36 | 37 | raise "Your account is in no teams" if teams.count == 0 38 | 39 | team_id = (ENV['FASTLANE_TEAM_ID'] || '').strip 40 | team_name = (ENV['FASTLANE_TEAM_NAME'] || '').strip 41 | 42 | if team_id.length > 0 43 | # User provided a value, let's see if it's valid 44 | teams.each_with_index do |team, i| 45 | # There are 2 different values - one from the login page one from the Dev Team Page 46 | return team['teamId'] if team['teamId'].strip == team_id 47 | return team['teamId'] if team['currentTeamMember']['teamMemberId'].to_s.strip == team_id 48 | end 49 | puts "Couldn't find team with ID '#{team_id}'" 50 | end 51 | 52 | if team_name.length > 0 53 | # User provided a value, let's see if it's valid 54 | teams.each_with_index do |team, i| 55 | return team['teamId'] if team['name'].strip == team_name 56 | end 57 | puts "Couldn't find team with Name '#{team_name}'" 58 | end 59 | 60 | return teams[0]['teamId'] if teams.count == 1 # user is just in one team 61 | 62 | # User Selection 63 | loop do 64 | # Multiple teams, user has to select 65 | puts "Multiple teams found, please enter the number of the team you want to use: " 66 | teams.each_with_index do |team, i| 67 | puts "#{i + 1}) #{team['teamId']} \"#{team['name']}\" (#{team['type']})" 68 | end 69 | 70 | selected = ($stdin.gets || '').strip.to_i - 1 71 | team_to_use = teams[selected] if selected >= 0 72 | 73 | return team_to_use['teamId'] if team_to_use 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/portal/fixtures/listTeams_multiple.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-24T23:33:38Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/listTeams.action", 8 | "responseId": "6f4c5dcb-ecd0-460c-967b-dcd81f7be315", 9 | "isAdmin": null, 10 | "isMember": null, 11 | "isAgent": null, 12 | "pageNumber": null, 13 | "pageSize": null, 14 | "totalRecords": null, 15 | "teams": [ 16 | { 17 | "status": "active", 18 | "teamId": "XXXXXXXXXX", 19 | "type": "Company/Organization", 20 | "extendedTeamAttributes": { 21 | }, 22 | "teamAgent": { 23 | "personId": 499707568, 24 | "firstName": "Felix", 25 | "lastName": "Krause", 26 | "email": "test@spaceship.com", 27 | "developerStatus": "active", 28 | "teamMemberId": "YYYYYYYYYY" 29 | }, 30 | "memberships": [ 31 | { 32 | "membershipId": "MMMMMMMMMM", 33 | "membershipProductId": "ds1", 34 | "status": "active", 35 | "inDeviceResetWindow": true, 36 | "inRenewalWindow": true, 37 | "dateStart": "02/23/15 03:38", 38 | "dateExpire": "02/23/16 07:59", 39 | "platform": "ios", 40 | "availableDeviceSlots": 100, 41 | "name": "iOS Developer Program" 42 | } 43 | ], 44 | "currentTeamMember": { 45 | "personId": null, 46 | "firstName": null, 47 | "lastName": null, 48 | "email": null, 49 | "developerStatus": null, 50 | "privileges": { 51 | }, 52 | "roles": [ 53 | "TEAM_ADMIN", 54 | "SAFARI_PROGRAM_TEAM_ADMIN" 55 | ], 56 | "teamMemberId": "YYYYYYYYYY" 57 | }, 58 | "name": "SpaceShip" 59 | }, 60 | { 61 | "status": "active", 62 | "teamId": "SecondTeam", 63 | "type": "In-House", 64 | "extendedTeamAttributes": { 65 | }, 66 | "teamAgent": { 67 | "personId": 499707511, 68 | "firstName": "Felix", 69 | "lastName": "Krause", 70 | "email": "test2@spaceship.com", 71 | "developerStatus": "active", 72 | "teamMemberId": "YYYYYYYYYY" 73 | }, 74 | "memberships": [ 75 | { 76 | "membershipId": "MMMMMMMMAA", 77 | "membershipProductId": "ds1", 78 | "status": "active", 79 | "inDeviceResetWindow": true, 80 | "inRenewalWindow": true, 81 | "dateStart": "02/23/15 03:38", 82 | "dateExpire": "02/23/16 07:59", 83 | "platform": "ios", 84 | "availableDeviceSlots": 100, 85 | "name": "iOS Developer Program" 86 | } 87 | ], 88 | "currentTeamMember": { 89 | "personId": null, 90 | "firstName": null, 91 | "lastName": null, 92 | "email": null, 93 | "developerStatus": null, 94 | "privileges": { 95 | }, 96 | "roles": [ 97 | "TEAM_ADMIN", 98 | "SAFARI_PROGRAM_TEAM_ADMIN" 99 | ], 100 | "teamMemberId": "YYYYYYYYYY" 101 | }, 102 | "name": "SecondTeamProfiName" 103 | } 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/testers/get_internal.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "testers": [{ 7 | "emailAddress": { 8 | "value": "detlef@krausefx.com", 9 | "isEditable": false, 10 | "isRequired": false, 11 | "errorKeys": null, 12 | "maxLength": 2147483647, 13 | "minLength": 0 14 | }, 15 | "firstName": { 16 | "value": "Detlef", 17 | "isEditable": false, 18 | "isRequired": false, 19 | "errorKeys": null, 20 | "maxLength": 2147483647, 21 | "minLength": 0 22 | }, 23 | "lastName": { 24 | "value": "Müller", 25 | "isEditable": false, 26 | "isRequired": false, 27 | "errorKeys": null, 28 | "maxLength": 2147483647, 29 | "minLength": 0 30 | }, 31 | "testerId": "4bb959dd-6a57-4aaf-b3ff-761041868817", 32 | "devices": [], 33 | "latestInstalledAppAdamId": null, 34 | "latestInstalledVersion": null, 35 | "latestInstalledShortVersion": null, 36 | "latestInstalledDate": null, 37 | "latestInstalledName": null, 38 | "latestInstallByPlatform": null, 39 | "hasAddedOrInvitedApp": null, 40 | "warnOnDelete": true, 41 | "itcUsername": "detlef@krausefx.com", 42 | "groups": null, 43 | "userName": "detlef@krausefx.com", 44 | "isActive": { 45 | "value": true, 46 | "isEditable": true, 47 | "isRequired": false, 48 | "errorKeys": null 49 | } 50 | }, { 51 | "emailAddress": { 52 | "value": "felix@sunapps.net", 53 | "isEditable": false, 54 | "isRequired": false, 55 | "errorKeys": null, 56 | "maxLength": 2147483647, 57 | "minLength": 0 58 | }, 59 | "firstName": { 60 | "value": "Felix", 61 | "isEditable": false, 62 | "isRequired": false, 63 | "errorKeys": null, 64 | "maxLength": 2147483647, 65 | "minLength": 0 66 | }, 67 | "lastName": { 68 | "value": "Krause", 69 | "isEditable": false, 70 | "isRequired": false, 71 | "errorKeys": null, 72 | "maxLength": 2147483647, 73 | "minLength": 0 74 | }, 75 | "testerId": "1d167b89-13c5-4dd8-b988-7a6a0190f774", 76 | "devices": [{ 77 | "model": "iPhone 6", 78 | "os": "iOS", 79 | "osVersion": "8.3", 80 | "name": null 81 | }], 82 | "latestInstalledAppAdamId": 794902327, 83 | "latestInstalledVersion": "0.9.14", 84 | "latestInstalledShortVersion": "1", 85 | "latestInstalledDate": 1427565638420, 86 | "latestInstalledName": "Golfclub Gut Altentann", 87 | "latestInstallByPlatform": null, 88 | "hasAddedOrInvitedApp": null, 89 | "warnOnDelete": true, 90 | "itcUsername": "felix@sunapps.net", 91 | "groups": null, 92 | "userName": "felix@sunapps.net", 93 | "isActive": { 94 | "value": true, 95 | "isEditable": true, 96 | "isRequired": false, 97 | "errorKeys": null 98 | } 99 | }], 100 | "devices": { 101 | "iPhone": ["iPhone 6"] 102 | } 103 | }, 104 | "messages": { 105 | "warn": null, 106 | "error": null, 107 | "info": null 108 | }, 109 | "statusCode": "SUCCESS" 110 | } -------------------------------------------------------------------------------- /spec/portal/fixtures/listCertRequests.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-04T08:31:38Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": "e1465eff-064e-4ad5-9b03-a333e4cfaaaa", 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/certificate/listCertRequests.action?content-type=text/x-url-arguments&accept=application/json&userLocale=en_US&certificateStatus=0", 8 | "responseId": "e2324663-c1ff-41ed-93e1-27d1f65d1aaaa", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": true, 12 | "pageNumber": 1, 13 | "pageSize": 500, 14 | "totalRecords": 3, 15 | "certRequests": [ 16 | { 17 | "certRequestId": "B23Q2P3AAA", 18 | "name": "SunApps GmbH", 19 | "statusString": "Issued", 20 | "dateRequestedString": "Nov 25, 2014", 21 | "dateRequested": "2014-11-25T22:55:42Z", 22 | "dateCreated": "2014-11-25T22:55:50Z", 23 | "expirationDate": "2015-11-25T22:45:50Z", 24 | "expirationDateString": "Nov 25, 2015", 25 | "ownerType": "team", 26 | "ownerName": "SunApps GmbH", 27 | "ownerId": "5A997XSAAA", 28 | "canDownload": true, 29 | "canRevoke": true, 30 | "certificateId": "XC5PH8DAAA", 31 | "certificateStatusCode": 0, 32 | "certRequestStatusCode": 4, 33 | "certificateTypeDisplayId": "R58UK2EAAA", 34 | "serialNum": "797E732CCE8B7AAA", 35 | "typeString": "iOS Distribution" 36 | }, 37 | { 38 | "certRequestId": "QTZF38GAAA", 39 | "name": "net.sunapps.54", 40 | "statusString": "Issued", 41 | "dateRequestedString": "Apr 02, 2015", 42 | "dateRequested": "2015-04-02T21:33:51Z", 43 | "dateCreated": "2015-04-02T21:34:00Z", 44 | "expirationDate": "2016-04-01T21:24:00Z", 45 | "expirationDateString": "Apr 01, 2016", 46 | "ownerType": "bundle", 47 | "ownerName": "Timelack", 48 | "ownerId": "3599RCHAAA", 49 | "canDownload": true, 50 | "canRevoke": true, 51 | "certificateId": "32KPRBAAAA", 52 | "certificateStatusCode": 0, 53 | "certRequestStatusCode": 4, 54 | "certificateTypeDisplayId": "UPV3DW712I", 55 | "serialNum": "7CFA36F20F058AAA", 56 | "typeString": "APNs Production iOS" 57 | }, 58 | { 59 | "certRequestId": "SPAZZUBAAA", 60 | "name": "SunApps GmbH", 61 | "statusString": "Issued", 62 | "dateRequestedString": "Feb 10, 2015", 63 | "dateRequested": "2015-02-10T23:54:14Z", 64 | "dateCreated": "2015-02-10T23:54:20Z", 65 | "expirationDate": "2016-02-10T23:44:20Z", 66 | "expirationDateString": "Feb 10, 2016", 67 | "ownerType": "team", 68 | "ownerName": "SunApps GmbH", 69 | "ownerId": "5A997XAAA", 70 | "canDownload": true, 71 | "canRevoke": true, 72 | "certificateId": "LHNT9C2AAA", 73 | "certificateStatusCode": 0, 74 | "certRequestStatusCode": 4, 75 | "certificateTypeDisplayId": "R58UK2EWSO", 76 | "serialNum": "7A52B44BBAEEEAAA", 77 | "typeString": "iOS Distribution" 78 | } 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/app_overview.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "adamId": "1039164429", 7 | "primaryLocaleCode": "en-US", 8 | "features": ["PRERELEASE", "PRICING", "GAMECENTER"], 9 | "appTransferState": null, 10 | "localizedMetadata": [{ 11 | "localeCode": "en-US", 12 | "name": "Updated by fastlane" 13 | }, { 14 | "localeCode": "de-DE", 15 | "name": "why new itc 2" 16 | }], 17 | "platforms": [{ 18 | "type": "APP", 19 | "platformString": "ios", 20 | "inFlightVersion": { 21 | "type": "APP", 22 | "id": "813314674", 23 | "version": "1.0", 24 | "state": "prepareForUpload", 25 | "stateKey": "prepareForUpload", 26 | "stateGroup": "preRelease", 27 | "largeAppIcon": { 28 | "assetToken": "Purple3/v4/42/45/f2/4245f2ee-3a37-a34d-a659-1f5f403ab2fe/pr_source.png", 29 | "sortOrder": null, 30 | "url": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/42/45/f2/4245f2ee-3a37-a34d-a659-1f5f403ab2fe/pr_source.png/1024x1024ss-80.png", 31 | "thumbNailUrl": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/42/45/f2/4245f2ee-3a37-a34d-a659-1f5f403ab2fe/pr_source.png/500x500bb-80.png", 32 | "originalFileName": "noalpha.png" 33 | }, 34 | "supportedHardware": ["iphone"], 35 | "issuesCount": 0 36 | }, 37 | "deliverableVersion": { 38 | "type": "APP", 39 | "id": "113314675", 40 | "version": "1.1", 41 | "state": "...", 42 | "stateKey": "...", 43 | "stateGroup": "...", 44 | "largeAppIcon": { 45 | "assetToken": "Purple3/v4/42/45/f2/4245f2ee-3a37-a34d-a659-1f5f403ab2fe/pr_source.png", 46 | "sortOrder": null, 47 | "url": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/42/45/f2/4245f2ee-3a37-a34d-a659-1f5f403ab2fe/pr_source.png/1024x1024ss-80.png", 48 | "thumbNailUrl": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/42/45/f2/4245f2ee-3a37-a34d-a659-1f5f403ab2fe/pr_source.png/500x500bb-80.png", 49 | "originalFileName": "noalpha.png" 50 | }, 51 | "supportedHardware": ["iphone"], 52 | "issuesCount": 0 53 | } 54 | },{ 55 | "type": "APP", 56 | "platformString": "appletvos", 57 | "inFlightVersion": { 58 | "type": "APP", 59 | "id": "814995239", 60 | "version": "1.0", 61 | "state": "prepareForUpload", 62 | "stateKey": "prepareForUpload", 63 | "stateGroup": "preRelease", 64 | "largeAppIcon": { 65 | "assetToken": null, 66 | "sortOrder": null, 67 | "url": null, 68 | "thumbNailUrl": null, 69 | "originalFileName": null 70 | }, 71 | "supportedHardware": ["iphone"], 72 | "issuesCount": 0 73 | }, 74 | "deliverableVersion": null 75 | }], 76 | "allowedNewVersionPlatforms": [], 77 | "submittablePlatforms": ["ios", "osx", "appletvos"], 78 | "appStoreUrl": "https://itunes.apple.com/us/app/why-new-itc-2/id1039164429?ls=1&mt=8", 79 | "analyticsUrl": null, 80 | "salesAndTrendsUrl": null, 81 | "canCreateAddOn": true, 82 | "everSubmittedAnAddon": false, 83 | "everSubmittedFreeSubscriptionAddon": true, 84 | "freeSubscriptionAddonCount": 0 85 | }, 86 | "messages": { 87 | "warn": null, 88 | "error": null, 89 | "info": null 90 | }, 91 | "statusCode": "SUCCESS" 92 | } -------------------------------------------------------------------------------- /spec/tunes/build_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Tunes::Build do 4 | before { Spaceship::Tunes.login } 5 | subject { Spaceship::Tunes.client } 6 | let(:username) { 'spaceship@krausefx.com' } 7 | let(:password) { 'so_secret' } 8 | 9 | describe "properly parses the build from the train" do 10 | let(:app) { Spaceship::Application.all.first } 11 | 12 | it "inspect works" do 13 | expect(Spaceship::Application.all.first.build_trains.values.first.builds.first.inspect).to include("Tunes::Build") 14 | end 15 | 16 | it "filled in all required values" do 17 | train = app.build_trains.values.first 18 | build = train.builds.first 19 | 20 | expect(build.build_train).to eq(train) 21 | expect(build.upload_date).to eq(1_443_144_470_000) 22 | expect(build.valid).to eq(true) 23 | expect(build.id).to eq(5_577_102) 24 | expect(build.build_version).to eq("10") 25 | expect(build.train_version).to eq("1.0") 26 | expect(build.icon_url).to eq('https://is3-ssl.mzstatic.com/image/thumb/Newsstand3/v4/94/80/28/948028c9-59e7-7b29-e75b-f57e97421ece/Icon-76@2x.png.png/150x150bb-80.png') 27 | expect(build.app_name).to eq('Updated by fastlane') 28 | expect(build.platform).to eq('ios') 29 | expect(build.internal_expiry_date).to eq(1_445_737_214_000) 30 | expect(build.external_expiry_date).to eq(0) 31 | expect(build.internal_testing_enabled).to eq(true) 32 | expect(build.external_testing_enabled).to eq(false) 33 | expect(build.watch_kit_enabled).to eq(false) 34 | expect(build.ready_to_install).to eq(true) 35 | 36 | # Analytics 37 | expect(build.install_count).to eq(0) 38 | expect(build.internal_install_count).to eq(0) 39 | expect(build.external_install_count).to eq(0) 40 | expect(build.session_count).to eq(0) 41 | expect(build.crash_count).to eq(0) 42 | end 43 | 44 | describe "#testing_status" do 45 | before do 46 | now = Time.at(1_444_440_842) 47 | allow(Time).to receive(:now) { now } 48 | end 49 | 50 | it "properly describes a build" do 51 | build1 = app.build_trains.values.first.builds.first 52 | expect(build1.testing_status).to eq("Internal") 53 | 54 | build2 = app.build_trains.values.last.builds.first 55 | expect(build2.testing_status).to eq("Inactive") 56 | end 57 | end 58 | 59 | describe "submitting/rejecting a build" do 60 | before do 61 | train = app.build_trains.values.first 62 | @build = train.builds.first 63 | end 64 | 65 | it "#cancel_beta_review!" do 66 | @build.cancel_beta_review! 67 | end 68 | 69 | it "#submit_for_beta_review!" do 70 | r = @build.submit_for_beta_review!({ 71 | changelog: "Custom Changelog" 72 | }) 73 | 74 | expect(r).to eq({ 75 | app_id: "898536088", 76 | train: "1.0", 77 | build_number: "10", 78 | changelog: "Custom Changelog", 79 | description: "No app description provided", 80 | feedback_email: "contact@company.com", 81 | marketing_url: "http://marketing.com", 82 | first_name: "Felix", 83 | last_name: "Krause", 84 | review_email: "contact@company.com", 85 | phone_number: "0123456789", 86 | significant_change: false, 87 | privacy_policy_url: nil, 88 | review_user_name: nil, 89 | review_password: nil, 90 | encryption: false }) 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/app_submission/start_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "adIdInfo": { 4 | "limitsTracking": { 5 | "errorKeys": null, 6 | "isEditable": false, 7 | "isRequired": false, 8 | "value": null 9 | }, 10 | "sectionErrorKeys": [], 11 | "sectionInfoKeys": [], 12 | "sectionWarningKeys": [], 13 | "servesAds": { 14 | "errorKeys": null, 15 | "isEditable": false, 16 | "isRequired": false, 17 | "value": null 18 | }, 19 | "tracksAction": { 20 | "errorKeys": null, 21 | "isEditable": false, 22 | "isRequired": false, 23 | "value": null 24 | }, 25 | "tracksInstall": { 26 | "errorKeys": null, 27 | "isEditable": false, 28 | "isRequired": false, 29 | "value": null 30 | }, 31 | "usesIdfa": { 32 | "errorKeys": null, 33 | "isEditable": false, 34 | "isRequired": true, 35 | "value": null 36 | } 37 | }, 38 | "contentRights": { 39 | "containsThirdPartyContent": { 40 | "errorKeys": null, 41 | "isEditable": false, 42 | "isRequired": true, 43 | "value": null 44 | }, 45 | "hasRights": { 46 | "errorKeys": null, 47 | "isEditable": false, 48 | "isRequired": false, 49 | "value": null 50 | } 51 | }, 52 | "exportCompliance": { 53 | "appType": "iOS App", 54 | "availableOnFrenchStore": { 55 | "errorKeys": null, 56 | "isEditable": true, 57 | "isRequired": false, 58 | "value": null 59 | }, 60 | "ccatFile": { 61 | "errorKeys": null, 62 | "isEditable": true, 63 | "isRequired": false, 64 | "value": null 65 | }, 66 | "containsProprietaryCryptography": { 67 | "errorKeys": null, 68 | "isEditable": true, 69 | "isRequired": false, 70 | "value": null 71 | }, 72 | "containsThirdPartyCryptography": { 73 | "errorKeys": null, 74 | "isEditable": true, 75 | "isRequired": false, 76 | "value": null 77 | }, 78 | "encryptionUpdated": null, 79 | "exportComplianceRequired": true, 80 | "isExempt": { 81 | "errorKeys": null, 82 | "isEditable": true, 83 | "isRequired": false, 84 | "value": null 85 | }, 86 | "platform": "ios", 87 | "sectionErrorKeys": [], 88 | "sectionInfoKeys": [], 89 | "sectionWarningKeys": [], 90 | "usesEncryption": { 91 | "errorKeys": null, 92 | "isEditable": true, 93 | "isRequired": false, 94 | "value": null 95 | } 96 | }, 97 | "previousPurchaseRestrictions": null, 98 | "versionInfo": null 99 | }, 100 | "messages": { 101 | "error": null, 102 | "info": null, 103 | "warn": null 104 | }, 105 | "statusCode": "SUCCESS" 106 | } 107 | -------------------------------------------------------------------------------- /lib/spaceship/du/utilities.rb: -------------------------------------------------------------------------------- 1 | require 'fastimage' 2 | require "English" 3 | 4 | module Spaceship 5 | # Set of utility methods useful to work with media files 6 | module Utilities #:nodoc: 7 | # Identifies the content_type of a file based on its file name extension. 8 | # Supports all formats required by DU-UTC right now (video, images and json) 9 | # @param path (String) the path to the file 10 | def content_type(path) 11 | path = path.downcase 12 | return 'image/jpeg' if path.end_with?('.jpg') 13 | return 'image/png' if path.end_with?('.png') 14 | return 'application/json' if path.end_with?('.geojson') 15 | return 'video/quicktime' if path.end_with?('.mov') 16 | return 'video/mp4' if path.end_with?('.m4v') 17 | return 'video/mp4' if path.end_with?('.mp4') 18 | raise "Unknown content-type for file #{path}" 19 | end 20 | 21 | # Identifies the resolution of a video or an image. 22 | # Supports all video and images required by DU-UTC right now 23 | # @param path (String) the path to the file 24 | def resolution(path) 25 | return FastImage.size(path) if content_type(path).start_with?("image") 26 | return video_resolution(path) if content_type(path).start_with?("video") 27 | raise "Cannot find resolution of file #{path}" 28 | end 29 | 30 | # Is the video or image in portrait mode ? 31 | # Supports all video and images required by DU-UTC right now 32 | # @param path (String) the path to the file 33 | def portrait?(path) 34 | resolution = resolution(path) 35 | resolution[0] < resolution[1] 36 | end 37 | 38 | # Grabs a screenshot from the specified video at the specified timestamp using `ffmpeg` 39 | # @param video_path (String) the path to the video file 40 | # @param timestamp (String) the `ffmpeg` timestamp format (e.g. 00.00) 41 | # @param dimensions (Array) the dimension of the screenshot to generate 42 | # @return the path to the TempFile containing the generated screenshot 43 | def grab_video_preview(video_path, timestamp, dimensions) 44 | width, height = dimensions 45 | require 'tempfile' 46 | tmp = Tempfile.new(['video_preview', ".jpg"]) 47 | file = tmp.path 48 | command = "ffmpeg -y -i \"#{video_path}\" -s #{width}x#{height} -ss \"#{timestamp}\" -vframes 1 \"#{file}\" 2>&1 >/dev/null" 49 | # puts "COMMAND: #{command}" 50 | `#{command}` 51 | raise "Failed to grab screenshot at #{timestamp} from #{video_path} (using #{command})" unless $CHILD_STATUS.to_i == 0 52 | tmp.path 53 | end 54 | 55 | # identifies the resolution of a video using `ffmpeg` 56 | # @param video_path (String) the path to the video file 57 | # @return [Array] the resolution of the video 58 | def video_resolution(video_path) 59 | command = "ffmpeg -i \"#{video_path}\" 2>&1" 60 | # puts "COMMAND: #{command}" 61 | output = `#{command}` 62 | # Note: ffmpeg exits with 1 if no output specified 63 | # raise "Failed to find video information from #{video_path} (using #{command})" unless $CHILD_STATUS.to_i == 0 64 | output = output.force_encoding("BINARY") 65 | video_infos = output.split("\n").select { |l| l =~ /Stream.*Video/ } 66 | raise "Unable to find Stream Video information from ffmpeg output of #{command}" if video_infos.count == 0 67 | video_info = video_infos[0] 68 | res = video_info.match(/.* ([0-9]+)x([0-9]+),.*/) 69 | raise "Unable to parse resolution information from #{video_info}" if res.count < 3 70 | [res[1].to_i, res[2].to_i] 71 | end 72 | 73 | module_function :content_type, :grab_video_preview, :portrait?, :resolution, :video_resolution 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/spaceship/launcher.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | class Launcher 3 | attr_accessor :client 4 | 5 | # Launch a new spaceship, which can be used to maintain multiple instances of 6 | # spaceship. You can call `.new` without any parameters, but you'll have to call 7 | # `.login` at a later point. If you prefer, you can pass the login credentials 8 | # here already. 9 | # 10 | # Authenticates with Apple's web services. This method has to be called once 11 | # to generate a valid session. The session will automatically be used from then 12 | # on. 13 | # 14 | # This method will automatically use the username from the Appfile (if available) 15 | # and fetch the password from the Keychain (if available) 16 | # 17 | # @param user (String) (optional): The username (usually the email address) 18 | # @param password (String) (optional): The password 19 | # 20 | # @raise InvalidUserCredentialsError: raised if authentication failed 21 | def initialize(user = nil, password = nil) 22 | @client = PortalClient.new 23 | 24 | if user or password 25 | @client.login(user, password) 26 | end 27 | end 28 | 29 | ##################################################### 30 | # @!group Login Helper 31 | ##################################################### 32 | 33 | # Authenticates with Apple's web services. This method has to be called once 34 | # to generate a valid session. The session will automatically be used from then 35 | # on. 36 | # 37 | # This method will automatically use the username from the Appfile (if available) 38 | # and fetch the password from the Keychain (if available) 39 | # 40 | # @param user (String) (optional): The username (usually the email address) 41 | # @param password (String) (optional): The password 42 | # 43 | # @raise InvalidUserCredentialsError: raised if authentication failed 44 | # 45 | # @return (Spaceship::Client) The client the login method was called for 46 | def login(user, password) 47 | @client.login(user, password) 48 | end 49 | 50 | # Open up the team selection for the user (if necessary). 51 | # 52 | # If the user is in multiple teams, a team selection is shown. 53 | # The user can then select a team by entering the number 54 | # 55 | # Additionally, the team ID is shown next to each team name 56 | # so that the user can use the environment variable `FASTLANE_TEAM_ID` 57 | # for future user. 58 | # 59 | # @return (String) The ID of the select team. You also get the value if 60 | # the user is only in one team. 61 | def select_team 62 | @client.select_team 63 | end 64 | 65 | ##################################################### 66 | # @!group Helper methods for managing multiple instances of spaceship 67 | ##################################################### 68 | 69 | # @return (Class) Access the apps for this spaceship 70 | def app 71 | Spaceship::App.set_client(@client) 72 | end 73 | 74 | # @return (Class) Access the app groups for this spaceship 75 | def app_group 76 | Spaceship::AppGroup.set_client(@client) 77 | end 78 | 79 | # @return (Class) Access the devices for this spaceship 80 | def device 81 | Spaceship::Device.set_client(@client) 82 | end 83 | 84 | # @return (Class) Access the certificates for this spaceship 85 | def certificate 86 | Spaceship::Certificate.set_client(@client) 87 | end 88 | 89 | # @return (Class) Access the provisioning profiles for this spaceship 90 | def provisioning_profile 91 | Spaceship::ProvisioningProfile.set_client(@client) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/portal/fixtures/listApps.action.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTimestamp": "2015-04-07T10:24:02Z", 3 | "resultCode": 0, 4 | "userLocale": "en_US", 5 | "protocolVersion": "QH65B2", 6 | "requestId": null, 7 | "requestUrl": "https://developer.apple.com:443//services-account/QH65B2/account/ios/identifiers/listAppIds.action", 8 | "responseId": "0d35cf9b-fe1e-4420-b94b-71dce02c2aaa", 9 | "isAdmin": true, 10 | "isMember": false, 11 | "isAgent": true, 12 | "pageNumber": 0, 13 | "pageSize": 0, 14 | "totalRecords": 25, 15 | "appIds": [ 16 | { 17 | "appIdId": "B7JBD8LHAA", 18 | "name": "The App Name", 19 | "appIdPlatform": "ios", 20 | "prefix": "5A997XSHK2", 21 | "identifier": "net.sunapps.151", 22 | "isWildCard": false, 23 | "isDuplicate": false, 24 | "features": {}, 25 | "enabledFeatures": [], 26 | "isDevPushEnabled": null, 27 | "isProdPushEnabled": null, 28 | "associatedApplicationGroupsCount": null, 29 | "associatedCloudContainersCount": null, 30 | "associatedIdentifiersCount": null 31 | }, 32 | { 33 | "appIdId": "2UMR2S6PAA", 34 | "name": "The Second App Name", 35 | "appIdPlatform": "ios", 36 | "prefix": "5A997XSHK2", 37 | "identifier": "net.sunapps.1", 38 | "isWildCard": false, 39 | "isDuplicate": false, 40 | "features": {}, 41 | "enabledFeatures": [], 42 | "isDevPushEnabled": null, 43 | "isProdPushEnabled": null, 44 | "associatedApplicationGroupsCount": null, 45 | "associatedCloudContainersCount": null, 46 | "associatedIdentifiersCount": null 47 | }, 48 | { 49 | "appIdId": "R9YNDTPLJX", 50 | "name": "GC Goehrde", 51 | "appIdPlatform": "ios", 52 | "prefix": "5A997XSHK2", 53 | "identifier": "net.sunapps.106", 54 | "isWildCard": false, 55 | "isDuplicate": false, 56 | "features": {}, 57 | "enabledFeatures": [], 58 | "isDevPushEnabled": null, 59 | "isProdPushEnabled": null, 60 | "associatedApplicationGroupsCount": null, 61 | "associatedCloudContainersCount": null, 62 | "associatedIdentifiersCount": null 63 | }, 64 | { 65 | "appIdId": "ZCN5GJG9AA", 66 | "name": "Xcode iOS Wildcard App ID", 67 | "appIdPlatform": "ios", 68 | "prefix": "5A997XSHK2", 69 | "identifier": "*", 70 | "isWildCard": true, 71 | "isDuplicate": false, 72 | "features": {}, 73 | "enabledFeatures": [], 74 | "isDevPushEnabled": null, 75 | "isProdPushEnabled": null, 76 | "associatedApplicationGroupsCount": null, 77 | "associatedCloudContainersCount": null, 78 | "associatedIdentifiersCount": null 79 | }, 80 | { 81 | "appIdId": "L42E9BTRAA", 82 | "name": "SunApps", 83 | "appIdPlatform": "ios", 84 | "prefix": "5A997XSHK2", 85 | "identifier": "net.sunapps.*", 86 | "isWildCard": true, 87 | "isDuplicate": false, 88 | "features": {}, 89 | "enabledFeatures": [], 90 | "isDevPushEnabled": null, 91 | "isProdPushEnabled": null, 92 | "associatedApplicationGroupsCount": null, 93 | "associatedCloudContainersCount": null, 94 | "associatedIdentifiersCount": null 95 | } 96 | ] 97 | } -------------------------------------------------------------------------------- /lib/spaceship/portal/spaceship.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Portal 3 | class << self 4 | # This client stores the default client when using the lazy syntax 5 | # Spaceship.app instead of using the spaceship launcher 6 | attr_accessor :client 7 | 8 | # Authenticates with Apple's web services. This method has to be called once 9 | # to generate a valid session. The session will automatically be used from then 10 | # on. 11 | # 12 | # This method will automatically use the username from the Appfile (if available) 13 | # and fetch the password from the Keychain (if available) 14 | # 15 | # @param user (String) (optional): The username (usually the email address) 16 | # @param password (String) (optional): The password 17 | # 18 | # @raise InvalidUserCredentialsError: raised if authentication failed 19 | # 20 | # @return (Spaceship::Client) The client the login method was called for 21 | def login(user = nil, password = nil) 22 | @client = PortalClient.login(user, password) 23 | end 24 | 25 | # Open up the team selection for the user (if necessary). 26 | # 27 | # If the user is in multiple teams, a team selection is shown. 28 | # The user can then select a team by entering the number 29 | # 30 | # Additionally, the team ID is shown next to each team name 31 | # so that the user can use the environment variable `FASTLANE_TEAM_ID` 32 | # for future user. 33 | # 34 | # @return (String) The ID of the select team. You also get the value if 35 | # the user is only in one team. 36 | def select_team 37 | @client.select_team 38 | end 39 | 40 | # Helper methods for managing multiple instances of spaceship 41 | 42 | # @return (Class) Access the apps for the spaceship 43 | def app 44 | Spaceship::App.set_client(@client) 45 | end 46 | 47 | # @return (Class) Access the app groups for the spaceship 48 | def app_group 49 | Spaceship::AppGroup.set_client(@client) 50 | end 51 | 52 | # @return (Class) Access app services for the spaceship 53 | def app_service 54 | Spaceship::AppService 55 | end 56 | 57 | # @return (Class) Access the devices for the spaceship 58 | def device 59 | Spaceship::Device.set_client(@client) 60 | end 61 | 62 | # @return (Class) Access the certificates for the spaceship 63 | def certificate 64 | Spaceship::Certificate.set_client(@client) 65 | end 66 | 67 | # @return (Class) Access the provisioning profiles for the spaceship 68 | def provisioning_profile 69 | Spaceship::ProvisioningProfile.set_client(@client) 70 | end 71 | end 72 | end 73 | 74 | # Legacy code to support `Spaceship.app` without `Portal` 75 | class << self 76 | def login(user = nil, password = nil) 77 | Spaceship::Portal.login(user, password) 78 | end 79 | 80 | def select_team 81 | Spaceship::Portal.select_team 82 | end 83 | 84 | def app 85 | Spaceship::Portal.app 86 | end 87 | 88 | def app_group 89 | Spaceship::Portal.app_group 90 | end 91 | 92 | def app_service 93 | Spaceship::Portal.app_service 94 | end 95 | 96 | def device 97 | Spaceship::Portal.device 98 | end 99 | 100 | def certificate 101 | Spaceship::Portal.certificate 102 | end 103 | 104 | def provisioning_profile 105 | Spaceship::Portal.provisioning_profile 106 | end 107 | 108 | def client 109 | Spaceship::Portal.client 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/app_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "adamId": "1039164429", 7 | "localizedMetadata": { 8 | "value": [{ 9 | "localeCode": "en-US", 10 | "name": { 11 | "value": "Updated by fastlane", 12 | "isEditable": true, 13 | "isRequired": true, 14 | "errorKeys": null, 15 | "maxLength": 255, 16 | "minLength": 2 17 | }, 18 | "privacyPolicyUrl": { 19 | "value": "https://fastlane.tools", 20 | "isEditable": true, 21 | "isRequired": false, 22 | "errorKeys": null, 23 | "maxLength": 255, 24 | "minLength": 0 25 | }, 26 | "privacyPolicy": { 27 | "value": null, 28 | "isEditable": false, 29 | "isRequired": false, 30 | "errorKeys": null 31 | } 32 | }, { 33 | "localeCode": "de-DE", 34 | "name": { 35 | "value": "why new itc 2", 36 | "isEditable": true, 37 | "isRequired": true, 38 | "errorKeys": null, 39 | "maxLength": 255, 40 | "minLength": 2 41 | }, 42 | "privacyPolicyUrl": { 43 | "value": null, 44 | "isEditable": true, 45 | "isRequired": false, 46 | "errorKeys": null, 47 | "maxLength": 255, 48 | "minLength": 0 49 | }, 50 | "privacyPolicy": { 51 | "value": null, 52 | "isEditable": false, 53 | "isRequired": false, 54 | "errorKeys": null 55 | } 56 | }], 57 | "isEditable": true, 58 | "isRequired": true, 59 | "errorKeys": null 60 | }, 61 | "availablePrimaryLocaleCodes": ["en-US", "de-DE"], 62 | "primaryLocaleCode": { 63 | "value": "en-US", 64 | "isEditable": true, 65 | "isRequired": true, 66 | "errorKeys": null 67 | }, 68 | "primaryCategory": { 69 | "value": "MZGenre.Sports", 70 | "isEditable": true, 71 | "isRequired": false, 72 | "errorKeys": null 73 | }, 74 | "primaryFirstSubCategory": { 75 | "value": null, 76 | "isEditable": true, 77 | "isRequired": false, 78 | "errorKeys": null 79 | }, 80 | "primarySecondSubCategory": { 81 | "value": null, 82 | "isEditable": true, 83 | "isRequired": false, 84 | "errorKeys": null 85 | }, 86 | "secondaryCategory": { 87 | "value": "MZGenre.Games", 88 | "isEditable": true, 89 | "isRequired": false, 90 | "errorKeys": null 91 | }, 92 | "secondaryFirstSubCategory": { 93 | "value": "MZGenre.Educational", 94 | "isEditable": true, 95 | "isRequired": false, 96 | "errorKeys": null 97 | }, 98 | "secondarySecondSubCategory": { 99 | "value": "MZGenre.Puzzle", 100 | "isEditable": true, 101 | "isRequired": false, 102 | "errorKeys": null 103 | }, 104 | "availableBundleIds": null, 105 | "bundleId": { 106 | "value": "tools.fastlane.app", 107 | "isEditable": false, 108 | "isRequired": true, 109 | "errorKeys": null 110 | }, 111 | "bundleIdSuffix": { 112 | "value": null, 113 | "isEditable": false, 114 | "isRequired": true, 115 | "errorKeys": null 116 | }, 117 | "license": { 118 | "countries": null, 119 | "isEditable": true, 120 | "isRequired": false, 121 | "errorKeys": null, 122 | "isEmptyValue": true, 123 | "EULAText": null 124 | }, 125 | "vendorId": "1441930512", 126 | "rating": null, 127 | "ageBandMinAge": null, 128 | "ageBandMaxAge": null, 129 | "countryRatings": null 130 | }, 131 | "messages": { 132 | "warn": null, 133 | "error": null, 134 | "info": null 135 | }, 136 | "statusCode": "SUCCESS" 137 | } -------------------------------------------------------------------------------- /lib/spaceship/tunes/app_details.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | class AppDetails < TunesBase 4 | attr_accessor :application 5 | 6 | #### 7 | # Localized values 8 | #### 9 | 10 | # @return (Array) Raw access the all available languages. You shouldn't use it probbaly 11 | attr_accessor :languages 12 | 13 | # @return (Hash) A hash representing the app name in all languages 14 | attr_reader :name 15 | 16 | # @return (Hash) A hash representing the privacy URL in all languages 17 | attr_reader :privacy_url 18 | 19 | # @return (Hash) Some bla bla about privacy 20 | attr_reader :apple_tv_privacy_policy 21 | 22 | # Categories (e.g. MZGenre.Business) 23 | attr_accessor :primary_category 24 | 25 | attr_accessor :primary_first_sub_category 26 | 27 | attr_accessor :primary_second_sub_category 28 | 29 | attr_accessor :secondary_category 30 | 31 | attr_accessor :secondary_first_sub_category 32 | 33 | attr_accessor :secondary_second_sub_category 34 | 35 | attr_mapping( 36 | 'localizedMetadata.value' => :languages, 37 | 'primaryCategory.value' => :primary_category, 38 | 'primaryFirstSubCategory.value' => :primary_first_sub_category, 39 | 'primarySecondSubCategory.value' => :primary_second_sub_category, 40 | 'secondaryCategory.value' => :secondary_category, 41 | 'secondaryFirstSubCategory.value' => :secondary_first_sub_category, 42 | 'secondarySecondSubCategory.value' => :secondary_second_sub_category 43 | ) 44 | 45 | class << self 46 | # Create a new object based on a hash. 47 | # This is used to create a new object based on the server response. 48 | def factory(attrs) 49 | obj = self.new(attrs) 50 | obj.unfold_languages 51 | 52 | return obj 53 | end 54 | end 55 | 56 | # Prefill name, privacy url 57 | def unfold_languages 58 | { 59 | name: :name, 60 | privacyPolicyUrl: :privacy_url, 61 | privacyPolicy: :apple_tv_privacy_policy 62 | }.each do |json, attribute| 63 | instance_variable_set("@#{attribute}".to_sym, LanguageItem.new(json, languages)) 64 | end 65 | end 66 | 67 | # Push all changes that were made back to iTunes Connect 68 | def save! 69 | client.update_app_details!(application.apple_id, raw_data) 70 | rescue Spaceship::TunesClient::ITunesConnectError => ex 71 | if ex.to_s == "operation_failed" 72 | # That's alright, we get this error message if nothing has changed 73 | else 74 | raise ex 75 | end 76 | end 77 | 78 | # Custom Setters 79 | # 80 | def primary_category=(value) 81 | value = "MZGenre.#{value}" unless value.include? "MZGenre" 82 | super(value) 83 | end 84 | 85 | def primary_first_sub_category=(value) 86 | value = "MZGenre.#{value}" unless value.include? "MZGenre" 87 | super(value) 88 | end 89 | 90 | def primary_second_sub_category=(value) 91 | value = "MZGenre.#{value}" unless value.include? "MZGenre" 92 | super(value) 93 | end 94 | 95 | def secondary_category=(value) 96 | value = "MZGenre.#{value}" unless value.include? "MZGenre" 97 | super(value) 98 | end 99 | 100 | def secondary_first_sub_category=(value) 101 | value = "MZGenre.#{value}" unless value.include? "MZGenre" 102 | super(value) 103 | end 104 | 105 | def secondary_second_sub_category=(value) 106 | value = "MZGenre.#{value}" unless value.include? "MZGenre" 107 | super(value) 108 | end 109 | 110 | ##################################################### 111 | # @!group General 112 | ##################################################### 113 | def setup 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /spec/du/fixtures/upload_trailer_response_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "responses" : [ { 3 | "url" : "http://a1846.phobos.apple.com/us/r30/VideoSource40/v4/e3/48/1a/e3481a8f-ec25-e19f-5048-270d7acaf89a/pr_source.mov?downloadKey=1438619697_55da9f1a93478be2a96468b3c4dda992", 4 | "token" : "VideoSource40/v4/e3/48/1a/e3481a8f-ec25-e19f-5048-270d7acaf89a/pr_source.mov", 5 | "groupToken" : { 6 | "groupToken" : "VideoSource40/v4/e3/48/1a/e3481a8f-ec25-e19f-5048-270d7acaf89a", 7 | "pool" : "VideoSource40", 8 | "syntaxVersion" : "HASH_MOUNT_UUID_GROUP" 9 | }, 10 | "blobString" : "{\"token\":\"VideoSource40/v4/e3/48/1a/e3481a8f-ec25-e19f-5048-270d7acaf89a/pr_source.mov\",\"type\":\"MZPurpleSoftwarePromoVideoFileType.SOURCE\",\"originalFileName\":\"preview dbn 5-900-note.mov\"}", 11 | "descriptionDoc" : "FoghornLeghorn Quicktime Parser1.242eng0469102301871122.16198199aac LR48.0" 12 | } ] 13 | } -------------------------------------------------------------------------------- /spec/portal/app_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Portal::App do 4 | before { Spaceship.login } 5 | let(:client) { Spaceship::Portal::App.client } 6 | 7 | describe "successfully loads and parses all apps" do 8 | it "the number is correct" do 9 | expect(Spaceship::Portal::App.all.count).to eq(5) 10 | end 11 | 12 | it "inspect works" do 13 | expect(Spaceship::Portal::App.all.first.inspect).to include("Portal::App") 14 | end 15 | 16 | it "parses app correctly" do 17 | app = Spaceship::Portal::App.all.first 18 | 19 | expect(app.app_id).to eq("B7JBD8LHAA") 20 | expect(app.name).to eq("The App Name") 21 | expect(app.platform).to eq("ios") 22 | expect(app.prefix).to eq("5A997XSHK2") 23 | expect(app.bundle_id).to eq("net.sunapps.151") 24 | expect(app.is_wildcard).to eq(false) 25 | end 26 | 27 | it "parses wildcard apps correctly" do 28 | app = Spaceship::Portal::App.all.last 29 | 30 | expect(app.app_id).to eq("L42E9BTRAA") 31 | expect(app.name).to eq("SunApps") 32 | expect(app.platform).to eq("ios") 33 | expect(app.prefix).to eq("5A997XSHK2") 34 | expect(app.bundle_id).to eq("net.sunapps.*") 35 | expect(app.is_wildcard).to eq(true) 36 | end 37 | 38 | it "parses app details correctly" do 39 | app = Spaceship::Portal::App.all.first 40 | app = app.details 41 | 42 | expect(app.app_id).to eq("B7JBD8LHAA") 43 | expect(app.name).to eq("The App Name") 44 | expect(app.platform).to eq("ios") 45 | expect(app.prefix).to eq("5A997XSHK2") 46 | expect(app.bundle_id).to eq("net.sunapps.151") 47 | expect(app.is_wildcard).to eq(false) 48 | 49 | expect(app.features).to include("push" => true) 50 | expect(app.enabled_features).to include("push") 51 | expect(app.dev_push_enabled).to eq(false) 52 | expect(app.prod_push_enabled).to eq(true) 53 | expect(app.app_groups_count).to eq(0) 54 | expect(app.cloud_containers_count).to eq(0) 55 | expect(app.identifiers_count).to eq(0) 56 | end 57 | 58 | it "allows modification of values and properly retrieving them" do 59 | app = Spaceship::App.all.first 60 | app.name = "12" 61 | expect(app.name).to eq("12") 62 | end 63 | end 64 | 65 | describe "Filter app based on app identifier" do 66 | it "works with specific App IDs" do 67 | app = Spaceship::Portal::App.find("net.sunapps.151") 68 | expect(app.app_id).to eq("B7JBD8LHAA") 69 | expect(app.is_wildcard).to eq(false) 70 | end 71 | 72 | it "works with wilcard App IDs" do 73 | app = Spaceship::Portal::App.find("net.sunapps.*") 74 | expect(app.app_id).to eq("L42E9BTRAA") 75 | expect(app.is_wildcard).to eq(true) 76 | end 77 | 78 | it "returns nil app ID wasn't found" do 79 | expect(Spaceship::Portal::App.find("asdfasdf")).to be_nil 80 | end 81 | end 82 | 83 | describe '#create' do 84 | it 'creates an app id with an explicit bundle_id' do 85 | expect(client).to receive(:create_app!).with(:explicit, 'Production App', 'tools.fastlane.spaceship.some-explicit-app', mac: false) { 86 | { 'isWildCard' => true } 87 | } 88 | app = Spaceship::Portal::App.create!(bundle_id: 'tools.fastlane.spaceship.some-explicit-app', name: 'Production App') 89 | expect(app.is_wildcard).to eq(true) 90 | end 91 | 92 | it 'creates an app id with a wildcard bundle_id' do 93 | expect(client).to receive(:create_app!).with(:wildcard, 'Development App', 'tools.fastlane.spaceship.*', mac: false) { 94 | { 'isWildCard' => false } 95 | } 96 | app = Spaceship::Portal::App.create!(bundle_id: 'tools.fastlane.spaceship.*', name: 'Development App') 97 | expect(app.is_wildcard).to eq(false) 98 | end 99 | end 100 | 101 | describe '#delete' do 102 | subject { Spaceship::Portal::App.find("net.sunapps.151") } 103 | it 'deletes the app by a given bundle_id' do 104 | expect(client).to receive(:delete_app!).with('B7JBD8LHAA', mac: false) 105 | app = subject.delete! 106 | expect(app.app_id).to eq('B7JBD8LHAA') 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/spaceship/tunes/build_train.rb: -------------------------------------------------------------------------------- 1 | module Spaceship 2 | module Tunes 3 | # Represents a build train of builds from iTunes Connect 4 | # A build train is all builds for a given version number with different build numbers 5 | class BuildTrain < TunesBase 6 | # @return (Spaceship::Tunes::Application) A reference to the application 7 | # this train is for 8 | attr_accessor :application 9 | 10 | # @return (Array) An array of all builds that are inside this train (Spaceship::Tunes::Build) 11 | attr_reader :builds 12 | 13 | # @return (String) The version number of this train 14 | attr_reader :version_string 15 | 16 | # @return (String) Platform (e.g. "ios") 17 | attr_reader :platform 18 | 19 | # @return (Bool) Is external beta testing enabled for this train? Only one train can have enabled testing. 20 | attr_reader :external_testing_enabled 21 | 22 | # @return (Bool) Is internal beta testing enabled for this train? Only one train can have enabled testing. 23 | attr_reader :internal_testing_enabled 24 | 25 | # @return (Array) An array of all builds that are inside this train (Spaceship::Tunes::Build) 26 | # I never got this to work to properly try and debug this 27 | attr_reader :processing_builds 28 | 29 | attr_mapping( 30 | 'versionString' => :version_string, 31 | 'platform' => :platform, 32 | 'externalTesting.value' => :external_testing_enabled, 33 | 'internalTesting.value' => :internal_testing_enabled 34 | ) 35 | 36 | class << self 37 | # Create a new object based on a hash. 38 | # This is used to create a new object based on the server response. 39 | def factory(attrs) 40 | self.new(attrs) 41 | end 42 | 43 | # @param application (Spaceship::Tunes::Application) The app this train is for 44 | # @param app_id (String) The unique Apple ID of this app 45 | def all(application, app_id) 46 | trains = [] 47 | trains += client.build_trains(app_id, 'internal')['trains'] 48 | trains += client.build_trains(app_id, 'external')['trains'] 49 | 50 | result = {} 51 | trains.each do |attrs| 52 | attrs.merge!(application: application) 53 | current = self.factory(attrs) 54 | result[current.version_string] = current 55 | end 56 | result 57 | end 58 | end 59 | 60 | # Setup all the builds and processing builds 61 | def setup 62 | super 63 | 64 | @builds = (self.raw_data['builds'] || []).collect do |attrs| 65 | attrs.merge!(build_train: self) 66 | Tunes::Build.factory(attrs) 67 | end 68 | 69 | @processing_builds = (self.raw_data['buildsInProcessing'] || []).collect do |attrs| 70 | attrs.merge!(build_train: self) 71 | Tunes::Build.factory(attrs) 72 | end 73 | 74 | # since buildsInProcessing appears empty, fallback to also including processing state from @builds 75 | @builds.each do |build| 76 | @processing_builds << build if build.processing == true && build.valid == true 77 | end 78 | end 79 | 80 | # @return (Spaceship::Tunes::Build) The latest build for this train, sorted by upload time. 81 | def latest_build 82 | @builds.max_by(&:upload_date) 83 | end 84 | 85 | # @param (testing_type) internal or external 86 | def update_testing_status!(new_value, testing_type, build = nil) 87 | data = client.build_trains(self.application.apple_id, testing_type) 88 | 89 | build ||= latest_build if testing_type == 'external' 90 | 91 | data['trains'].each do |train| 92 | train["#{testing_type}Testing"]['value'] = false 93 | train["#{testing_type}Testing"]['value'] = new_value if train['versionString'] == version_string 94 | 95 | # also update the builds 96 | train['builds'].each do |b| 97 | next if b["#{testing_type}Testing"].nil? 98 | next if build.nil? 99 | next if b["buildVersion"] != build.build_version 100 | b["#{testing_type}Testing"]['value'] = false 101 | b["#{testing_type}Testing"]['value'] = new_value if b['trainVersion'] == version_string 102 | end 103 | end 104 | 105 | result = client.update_build_trains!(application.apple_id, testing_type, data) 106 | self.internal_testing_enabled = new_value if testing_type == 'internal' 107 | self.external_testing_enabled = new_value if testing_type == 'external' 108 | 109 | result 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/tunes/fixtures/testflight_submission_start.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "sectionErrorKeys": [], 4 | "sectionInfoKeys": [], 5 | "sectionWarningKeys": [], 6 | "significantChange": { 7 | "value": false, 8 | "isEditable": true, 9 | "isRequired": false, 10 | "errorKeys": null, 11 | "maxLength": 1, 12 | "minLength": 1 13 | }, 14 | "build": { 15 | "sectionErrorKeys": [], 16 | "sectionInfoKeys": [], 17 | "sectionWarningKeys": [], 18 | "buildVersion": "99993", 19 | "trainVersion": "0.9.21", 20 | "uploadDate": 1437321531000, 21 | "iconUrl": "https://is3-ssl.mzstatic.com/image/thumb/Newsstand7/v4/af/d7/bf/afd7bf97-1eee-05dc-e9bb-dc3a07bbc96a/Icon-60@2x.png.png/150x150bb-80.png", 22 | "appName": "App", 23 | "platform": "ios", 24 | "betaEntitled": true 25 | }, 26 | "testInfo": { 27 | "sectionErrorKeys": [], 28 | "sectionInfoKeys": [], 29 | "sectionWarningKeys": [], 30 | "internalStatus": "active", 31 | "externalStatus": "submitForReview", 32 | "expirationDate": 1439972462000, 33 | "internalExpirationDate": 1439972462000, 34 | "externalExpirationDate": null, 35 | "primaryLanguage": "German", 36 | "availableLanguages": ["German"], 37 | "details": [{ 38 | "sectionErrorKeys": [], 39 | "sectionInfoKeys": [], 40 | "sectionWarningKeys": [], 41 | "language": "German", 42 | "whatsNew": { 43 | "value": "Such Change", 44 | "isEditable": true, 45 | "isRequired": true, 46 | "errorKeys": null, 47 | "maxLength": 4000, 48 | "minLength": 4 49 | }, 50 | "description": { 51 | "value": "Such Description", 52 | "isEditable": true, 53 | "isRequired": true, 54 | "errorKeys": null, 55 | "maxLength": 4000, 56 | "minLength": 10 57 | }, 58 | "feedbackEmail": { 59 | "value": "feedback@krausef.com", 60 | "isEditable": true, 61 | "isRequired": true, 62 | "errorKeys": null, 63 | "maxLength": 500, 64 | "minLength": 1 65 | }, 66 | "marketingUrl": { 67 | "value": "http://marketing.com", 68 | "isEditable": true, 69 | "isRequired": true, 70 | "errorKeys": null, 71 | "maxLength": 255, 72 | "minLength": 1 73 | }, 74 | "privacyPolicyUrl": { 75 | "value": null, 76 | "isEditable": true, 77 | "isRequired": false, 78 | "errorKeys": null, 79 | "maxLength": 255, 80 | "minLength": 1 81 | } 82 | }], 83 | "reviewFirstName": { 84 | "value": "Felix", 85 | "isEditable": true, 86 | "isRequired": true, 87 | "errorKeys": null, 88 | "maxLength": 100, 89 | "minLength": 1 90 | }, 91 | "reviewLastName": { 92 | "value": "Krause", 93 | "isEditable": true, 94 | "isRequired": true, 95 | "errorKeys": null, 96 | "maxLength": 100, 97 | "minLength": 1 98 | }, 99 | "reviewPhone": { 100 | "value": "0128383383", 101 | "isEditable": true, 102 | "isRequired": true, 103 | "errorKeys": null, 104 | "maxLength": 20, 105 | "minLength": 1 106 | }, 107 | "reviewEmail": { 108 | "value": "review@krausefx.com", 109 | "isEditable": true, 110 | "isRequired": true, 111 | "errorKeys": null, 112 | "maxLength": 100, 113 | "minLength": 1 114 | }, 115 | "reviewNotes": { 116 | "value": null, 117 | "isEditable": true, 118 | "isRequired": false, 119 | "errorKeys": null, 120 | "maxLength": 4000, 121 | "minLength": 1 122 | }, 123 | "reviewUserName": { 124 | "value": null, 125 | "isEditable": true, 126 | "isRequired": false, 127 | "errorKeys": null, 128 | "maxLength": 400, 129 | "minLength": 1 130 | }, 131 | "reviewPassword": { 132 | "value": null, 133 | "isEditable": true, 134 | "isRequired": false, 135 | "errorKeys": null, 136 | "maxLength": 400, 137 | "minLength": 1 138 | }, 139 | "hasExportCompliance": true, 140 | "installCount": 1, 141 | "externalInstallCount": 0, 142 | "internalInstallCount": 1, 143 | "sessionCount": 0, 144 | "externalSessionCount": 0, 145 | "internalSessionCount": 0, 146 | "crashCount": 0, 147 | "externalCrashCount": 0, 148 | "internalCrashCount": 0 149 | }, 150 | "nextBuildSubmissionAllowed": 0 151 | }, 152 | "messages": { 153 | "warn": null, 154 | "error": null, 155 | "info": ["Success"] 156 | }, 157 | "statusCode": "SUCCESS" 158 | } -------------------------------------------------------------------------------- /spec/portal/device_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spaceship::Device do 4 | before { Spaceship.login } 5 | let(:client) { Spaceship::Device.client } 6 | 7 | subject(:all_devices) { Spaceship::Device.all } 8 | it "successfully loads and parses all devices" do 9 | expect(all_devices.count).to eq(4) 10 | device = all_devices.first 11 | expect(device.id).to eq('AAAAAAAAAA') 12 | expect(device.name).to eq('Felix\'s iPhone') 13 | expect(device.udid).to eq('a03b816861e89fac0a4da5884cb9d2f01bd5641e') 14 | expect(device.platform).to eq('ios') 15 | expect(device.status).to eq('c') 16 | expect(device.model).to eq('iPhone 5 (Model A1428)') 17 | expect(device.device_type).to eq('iphone') 18 | end 19 | 20 | subject(:all_phones) { Spaceship::Device.all_iphones } 21 | it "successfully loads and parses all iPhones" do 22 | expect(all_phones.count).to eq(3) 23 | device = all_phones.first 24 | expect(device.id).to eq('AAAAAAAAAA') 25 | expect(device.name).to eq('Felix\'s iPhone') 26 | expect(device.udid).to eq('a03b816861e89fac0a4da5884cb9d2f01bd5641e') 27 | expect(device.platform).to eq('ios') 28 | expect(device.status).to eq('c') 29 | expect(device.model).to eq('iPhone 5 (Model A1428)') 30 | expect(device.device_type).to eq('iphone') 31 | end 32 | 33 | subject(:all_ipods) { Spaceship::Device.all_ipod_touches } 34 | it "successfully loads and parses all iPods" do 35 | expect(all_ipods.count).to eq(1) 36 | device = all_ipods.first 37 | expect(device.id).to eq('CCCCCCCCCC') 38 | expect(device.name).to eq('Personal iPhone') 39 | expect(device.udid).to eq('97467684eb8dfa3c6d272eac3890dab0d001c706') 40 | expect(device.platform).to eq('ios') 41 | expect(device.status).to eq('c') 42 | expect(device.model).to eq(nil) 43 | expect(device.device_type).to eq('ipod') 44 | end 45 | 46 | subject(:all_apple_tvs) { Spaceship::Device.all_apple_tvs } 47 | it "successfully loads and parses all Apple TVs" do 48 | expect(all_apple_tvs.count).to eq(1) 49 | device = all_apple_tvs.first 50 | expect(device.id).to eq('EEEEEEEEEE') 51 | expect(device.name).to eq('Tracy\'s Apple TV') 52 | expect(device.udid).to eq('8defe35b2cad44affacabd124834acbd8746ff34') 53 | expect(device.platform).to eq('ios') 54 | expect(device.status).to eq('c') 55 | expect(device.model).to eq('The new Apple TV') 56 | expect(device.device_type).to eq('tvOS') 57 | end 58 | 59 | subject(:all_watches) { Spaceship::Device.all_watches } 60 | it "successfully loads and parses all Watches" do 61 | expect(all_watches.count).to eq(1) 62 | device = all_watches.first 63 | expect(device.id).to eq('FFFFFFFFFF') 64 | expect(device.name).to eq('Tracy\'s Watch') 65 | expect(device.udid).to eq('8defe35b2cad44aff7d8e9dfe4ca4d2fb94ae509') 66 | expect(device.platform).to eq('ios') 67 | expect(device.status).to eq('c') 68 | expect(device.model).to eq('Apple Watch 38mm') 69 | expect(device.device_type).to eq('watch') 70 | end 71 | 72 | it "inspect works" do 73 | expect(subject.first.inspect).to include("Portal::Device") 74 | end 75 | 76 | describe "#find" do 77 | it "finds a device by its ID" do 78 | device = Spaceship::Device.find("AAAAAAAAAA") 79 | expect(device.id).to eq("AAAAAAAAAA") 80 | expect(device.udid).to eq("a03b816861e89fac0a4da5884cb9d2f01bd5641e") 81 | end 82 | end 83 | 84 | describe "#create" do 85 | it "should create and return a new device" do 86 | expect(client).to receive(:create_device!).with("Demo Device", "7f6c8dc83d77134b5a3a1c53f1202b395b04482b", mac: false).and_return({}) 87 | device = Spaceship::Device.create!(name: "Demo Device", udid: "7f6c8dc83d77134b5a3a1c53f1202b395b04482b") 88 | end 89 | 90 | it "should fail to create a nil device UDID" do 91 | expect do 92 | Spaceship::Device.create!(name: "Demo Device", udid: nil) 93 | end.to raise_error("You cannot create a device without a device_id (UDID) and name") 94 | end 95 | 96 | it "should fail to create a nil device name" do 97 | expect do 98 | Spaceship::Device.create!(name: nil, udid: "7f6c8dc83d77134b5a3a1c53f1202b395b04482b") 99 | end.to raise_error("You cannot create a device without a device_id (UDID) and name") 100 | end 101 | 102 | it "raises an exception if the device ID is already registererd" do 103 | expect do 104 | device = Spaceship::Device.create!(name: "Demo", udid: "e5814abb3b1d92087d48b64f375d8e7694932c39") 105 | end.to raise_error "The device UDID 'e5814abb3b1d92087d48b64f375d8e7694932c39' already exists on this team." 106 | end 107 | 108 | it "doesn't raise an exception if the device name is already registererd" do 109 | expect(client).to receive(:create_device!).with("Personal iPhone", "e5814abb3b1d92087d48b64f375d8e7694932c3c", mac: false).and_return({}) 110 | device = Spaceship::Device.create!(name: "Personal iPhone", udid: "e5814abb3b1d92087d48b64f375d8e7694932c3c") 111 | end 112 | end 113 | end 114 | --------------------------------------------------------------------------------