lib/spaceship/tunes/build_train.rb



module Spaceship
  module Tunes
    # Represents a build train of builds from iTunes Connect
    # A build train is all builds for a given version number with different build numbers
    class BuildTrain < TunesBase
      # @return (Spaceship::Tunes::Application) A reference to the application this train is for
      attr_accessor :application

      # @return (Array) An array of all builds that are inside this train (Spaceship::Tunes::Build)
      attr_reader :builds

      # @return (String) The version number of this train
      attr_reader :version_string

      # @return (String) Platform (e.g. "ios")
      attr_reader :platform

      # @return (Bool) Is external beta testing enabled for this train? Only one train can have enabled testing.
      attr_reader :external_testing_enabled

      # @return (Bool) Is internal beta testing enabled for this train? Only one train can have enabled testing.
      attr_reader :internal_testing_enabled

      # @return (Array) An array of all processing builds that are inside this train (Spaceship::Tunes::Build)
      # Does not include invalid builds.
      #  I never got this to work to properly try and debug this
      attr_reader :processing_builds

      # @return (Array) An array of all invalid builds that are inside this train
      attr_reader :invalid_builds

      attr_mapping(
        'versionString' => :version_string,
        'platform' => :platform,
        'externalTesting.value' => :external_testing_enabled,
        'internalTesting.value' => :internal_testing_enabled
      )

      class << self
        # Create a new object based on a hash.
        # This is used to create a new object based on the server response.
        def factory(attrs)
          self.new(attrs)
        end

        # @param application (Spaceship::Tunes::Application) The app this train is for
        # @param app_id (String) The unique Apple ID of this app
        def all(application, app_id)
          trains = []
          trains += client.build_trains(app_id, 'internal')['trains']
          trains += client.build_trains(app_id, 'external')['trains']

          result = {}
          trains.each do |attrs|
            attrs[:application] = application
            current = self.factory(attrs)
            result[current.version_string] = current
          end
          result
        end
      end

      # Setup all the builds and processing builds
      def setup
        super

        @builds = (self.raw_data['builds'] || []).collect do |attrs|
          attrs[:build_train] = self
          Tunes::Build.factory(attrs)
        end

        @invalid_builds = @builds.select do |build|
          build.processing_state == 'processingFailed' || build.processing_state == 'invalidBinary'
        end

        # This step may not be necessary anymore - it seems as if every processing build will be caught by the
        # @builds.each below, but not every processing build makes it to buildsInProcessing, so this is redundant
        @processing_builds = (self.raw_data['buildsInProcessing'] || []).collect do |attrs|
          attrs[:build_train] = self
          Tunes::Build.factory(attrs)
        end

        # since buildsInProcessing appears empty, fallback to also including processing state from @builds
        @builds.each do |build|
          # What combination of attributes constitutes which state is pretty complicated. The table below summarizes
          # what I've observed, but there's no reason to believe there aren't more states I just haven't seen yet.
          # The column headers are qualitative states of a given build, and the first column is the observed attributes
          # of that build.
          # NOTE: Some of the builds in the build_trains.json fixture do not follow these rules. I don't know if that is
          # because those examples are older, and the iTC API has changed, or if their format is still a possibility.
          # The second part of the OR clause in the line below exists so that those suspicious examples continue to be
          # accepted for unit tests.
          # +---------------------+-------------------+-------------------+-----------------+--------------------+---------+
          # |                     | just after upload | normal processing | invalid binary  | processing failed  | success |
          # +---------------------+-------------------+-------------------+-----------------+--------------------+---------+
          # |  build.processing = | true              | true              | true            | true               | false   |
          # |       build.valid = | false             | true              | false           | true               | true    |
          # | .processing_state = | "processing"      | "processing"      | "invalidBinary" | "processingFailed" | nil     |
          # +---------------------+-------------------+-------------------+-----------------+--------------------+---------+
          if build.processing_state == 'processing' || (build.processing && build.processing_state != 'invalidBinary' && build.processing_state != 'processingFailed')
            @processing_builds << build
          end
        end
      end

      # @return (Spaceship::Tunes::Build) The latest build for this train, sorted by upload time.
      def latest_build
        @builds.max_by(&:upload_date)
      end

      # @param (testing_type) internal or external
      def update_testing_status!(new_value, testing_type, build = nil)
        data = client.build_trains(self.application.apple_id, testing_type)

        build ||= latest_build if testing_type == 'external'
        testing_key = "#{testing_type}Testing"

        # Delete the irrelevant trains and update the relevant one to enable testing
        data['trains'].delete_if do |train|
          if train['versionString'] != version_string
            true
          else
            train[testing_key]['value'] = new_value

            # also update the builds
            train['builds'].delete_if do |b|
              if b[testing_key].nil?
                true
              elsif build && b["buildVersion"] == build.build_version
                b[testing_key]['value'] = new_value
                false
              elsif b[testing_key]['value'] == true
                b[testing_key]['value'] = false
                false
              else
                true
              end
            end

            false
          end
        end

        result = client.update_build_trains!(application.apple_id, testing_type, data)
        self.internal_testing_enabled = new_value if testing_type == 'internal'
        self.external_testing_enabled = new_value if testing_type == 'external'

        result
      end
    end
  end
end