device_api-ios 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5e0d6fee430df723821822f044a27761bec4932d
4
- data.tar.gz: a38729c9ef0cc3a0266ee14892ee0d83b942f284
3
+ metadata.gz: 44a319cbde8d0c83a2c6ce73f50a9f659381363e
4
+ data.tar.gz: 5a5251780401174266d85ce74f4c547f8736633b
5
5
  SHA512:
6
- metadata.gz: 610556e7181893924af71a2764d2e18e668c82f60514b1950308fa373e1fc4342d45f53e2e754a1fe36ef9738623cbb97cfa5370c8000361eea945922397ff92
7
- data.tar.gz: 8a8a987c173cf19d285d20307be932add6046ede1bedf419c8224ed53e4bca6a7914e9f9aee47ca31872667caf1f85af3f305086258918fe6a2c862843bb731e
6
+ metadata.gz: 62bd6649f4d4beceb65bbbae499a0daec718543557380eaefb6d6717c62369af546d50806961c720ee30ed8f22d20c8587777773ca842c2c3afe5f2e8110158b
7
+ data.tar.gz: 0654d20a10cc4b7d461c3d31253f50fa2ea090d9d3af46be195e62ef6cf9c2a7fbc0f7b4e1b1b637e2fab4985f0feb2276b165a502f0855f8c1cc5566bdc5a70
data/Gemfile CHANGED
@@ -1,12 +1,10 @@
1
1
  source 'https://rubygems.org'
2
- source 'https://gems.cloud.bbc.co.uk'
3
2
 
4
3
  gem 'device_api', '>=1.0.0'
5
4
  gem 'ios-devices'
5
+ gem 'ox'
6
6
 
7
7
  group :test do
8
8
  gem 'rspec'
9
- gem 'yaml'
10
- gem 'open3'
11
- gem 'ostruct'
9
+ gem 'pry'
12
10
  end
data/Gemfile.lock CHANGED
@@ -1,33 +1,37 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
- remote: https://gems.cloud.bbc.co.uk/
4
3
  specs:
5
- columnize (0.8.9)
6
- debugger (1.6.8)
7
- columnize (>= 0.3.1)
8
- debugger-linecache (~> 1.2.0)
9
- debugger-ruby_core_source (~> 1.3.5)
10
- debugger-linecache (1.2.0)
11
- debugger-ruby_core_source (1.3.5)
12
- device_api (1.0.0)
4
+ coderay (1.1.0)
5
+ device_api (1.0.1)
13
6
  diff-lcs (1.2.5)
14
- rspec (3.1.0)
15
- rspec-core (~> 3.1.0)
16
- rspec-expectations (~> 3.1.0)
17
- rspec-mocks (~> 3.1.0)
18
- rspec-core (3.1.4)
19
- rspec-support (~> 3.1.0)
20
- rspec-expectations (3.1.1)
7
+ ios-devices (0.2.1)
8
+ method_source (0.8.2)
9
+ ox (2.2.1)
10
+ pry (0.10.2)
11
+ coderay (~> 1.1.0)
12
+ method_source (~> 0.8.1)
13
+ slop (~> 3.4)
14
+ rspec (3.3.0)
15
+ rspec-core (~> 3.3.0)
16
+ rspec-expectations (~> 3.3.0)
17
+ rspec-mocks (~> 3.3.0)
18
+ rspec-core (3.3.2)
19
+ rspec-support (~> 3.3.0)
20
+ rspec-expectations (3.3.1)
21
21
  diff-lcs (>= 1.2.0, < 2.0)
22
- rspec-support (~> 3.1.0)
23
- rspec-mocks (3.1.1)
24
- rspec-support (~> 3.1.0)
25
- rspec-support (3.1.0)
22
+ rspec-support (~> 3.3.0)
23
+ rspec-mocks (3.3.2)
24
+ diff-lcs (>= 1.2.0, < 2.0)
25
+ rspec-support (~> 3.3.0)
26
+ rspec-support (3.3.0)
27
+ slop (3.6.0)
26
28
 
27
29
  PLATFORMS
28
30
  ruby
29
31
 
30
32
  DEPENDENCIES
31
- debugger
32
33
  device_api (>= 1.0.0)
34
+ ios-devices
35
+ ox
36
+ pry
33
37
  rspec
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'device_api-ios'
3
- s.version = '1.0.1'
3
+ s.version = '1.0.3'
4
4
  s.date = Time.now.strftime("%Y-%m-%d")
5
5
  s.summary = 'IOS Device Management API'
6
6
  s.description = 'iOS implementation of DeviceAPI'
@@ -10,5 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.homepage = 'https://github.com/bbc/device_api-ios'
11
11
  s.license = 'MIT'
12
12
  s.add_runtime_dependency 'device_api', '>=1.0', '<2.0'
13
+ s.add_runtime_dependency 'ios-devices', '>=0.2'
14
+ s.add_runtime_dependency 'ox', '>=2.1.0'
13
15
  s.add_development_dependency 'rspec'
14
16
  end
@@ -1,6 +1,13 @@
1
1
  require 'yaml'
2
2
  require 'device_api/ios/device'
3
3
  require 'device_api/ios/idevice'
4
+ require 'device_api/ios/ideviceinstaller'
5
+ require 'device_api/ios/signing'
6
+ require 'device_api/ios/plistutil'
7
+ require 'device_api/ios/idevicedebug'
8
+ require 'device_api/ios/ipaddress'
9
+ require 'device_api/ios/ideviceprovision'
10
+ require 'device_api/ios/idevicename'
4
11
 
5
12
  module DeviceAPI
6
13
  module IOS
@@ -19,7 +26,7 @@ module DeviceAPI
19
26
  if serial.to_s.empty?
20
27
  raise DeviceAPI::BadSerialString.new("Serial was '#{ serial.nil? ? 'nil' : serial }'")
21
28
  end
22
- DeviceAPI::IOS::Device.new(serial: serial, state: 'ok')
29
+ DeviceAPI::IOS::Device.new(serial: serial, state: 'device')
23
30
  end
24
31
  end
25
32
  end
@@ -1,6 +1,8 @@
1
1
  require 'device_api/device'
2
2
  require 'device_api/ios/device'
3
3
  require 'device_api/ios/idevice'
4
+ require 'device_api/ios/plistutil'
5
+ require 'device_api/ios/idevicename'
4
6
  require 'ios/devices'
5
7
 
6
8
  # DeviceAPI - an interface to allow for automation of devices
@@ -24,6 +26,12 @@ module DeviceAPI
24
26
  }[@state]
25
27
  end
26
28
 
29
+ # Look up device name - i.e. Bob's iPhone
30
+ # @return (String) iOS device name
31
+ def name
32
+ IDeviceName.name(serial)
33
+ end
34
+
27
35
  # Look up device model using the ios-devices gem - changing 'iPad4,7' to 'iPad mini 3'
28
36
  # @return (String) human readable model and version (where applicable)
29
37
  def model
@@ -54,6 +62,64 @@ module DeviceAPI
54
62
  IDevice.trusted?(serial)
55
63
  end
56
64
 
65
+ # Get the app bundle ID from the specified app
66
+ # @return [String] app bundle id
67
+ def package_name(app)
68
+ app_info = Plistutil.get_bundle_id_from_app(app)
69
+ app_info['CFBundleIdentifier']
70
+ end
71
+
72
+ # Get the app version from the specified app
73
+ # @return [String] app version
74
+ def app_version_number(app)
75
+ app_info = Plistutil.get_bundle_id_from_app(app)
76
+ app_info['CFBundleVersion']
77
+ end
78
+
79
+ # Get the IP Address from the device
80
+ # @return [String] IP Address of current device
81
+ def ip_address
82
+ IPAddress.address(serial)
83
+ end
84
+
85
+ # Get the Wifi Mac address for the current device
86
+ # @return [String] Mac address of current device
87
+ def wifi_mac_address
88
+ get_prop('WiFiAddress')
89
+ end
90
+
91
+ # Install a specified IPA
92
+ # @param [String] ipa string containing path to the IPA to install
93
+ # @return [Boolean, Exception] true when the IPA installed successfully, otherwise an error is raised
94
+ def install(ipa)
95
+ fail StandardError, 'No IPA or app specified.', caller if ipa.empty?
96
+
97
+ res = install_ipa(ipa)
98
+
99
+ fail StandardError, res, caller unless res
100
+ true
101
+ end
102
+
103
+ # Uninstall a specified package
104
+ # @param [String] package_name string containing name of package to uninstall
105
+ # @return [Boolean, Exception] true when the package is uninstalled successfully, otherwise an error is raised
106
+ def uninstall(package_name)
107
+ res = uninstall_package(package_name)
108
+
109
+ fail StandardError, res, caller unless res
110
+ true
111
+ end
112
+
113
+ # Return whether or not the device is a tablet or mobile
114
+ # @return [Symbol] :tablet or :mobile depending on device_class
115
+ def type
116
+ if device_class.downcase == 'ipad'
117
+ :tablet
118
+ else
119
+ :mobile
120
+ end
121
+ end
122
+
57
123
  private
58
124
 
59
125
  def get_prop(key)
@@ -63,6 +129,13 @@ module DeviceAPI
63
129
  @props[key]
64
130
  end
65
131
 
132
+ def install_ipa(ipa)
133
+ IDeviceInstaller.install_ipa(ipa: ipa, serial: serial)
134
+ end
135
+
136
+ def uninstall_package(package_name)
137
+ IDeviceInstaller.uninstall_package(package: package_name, serial: serial)
138
+ end
66
139
  end
67
140
  end
68
141
  end
@@ -30,7 +30,8 @@ module DeviceAPI
30
30
  # @return true if the device returns information to ideviceinfo, otherwise false
31
31
  def self.trusted?(device_id)
32
32
  result = execute("ideviceinfo -u '#{device_id}'")
33
- return true if result.exit == 0
33
+
34
+ return true if result.exit == 0 && !result.stdout.split("\n")[0].match('Usage')
34
35
  false
35
36
  end
36
37
 
@@ -0,0 +1,36 @@
1
+ # DeviceAPI - an interface to allow for automation of devices
2
+ module DeviceAPI
3
+ # iOS component of DeviceAPI
4
+ module IOS
5
+ # Namespace for all methods encapsulating idevice calls
6
+ class IDeviceDebug < Execution
7
+
8
+ # idevicedebug doesn't return until the app you are attempting to run
9
+ # exits. By passing in a timeout value we can limit how long we wait
10
+ # before terminating the debug session
11
+ # @param [Hash] options options for debug running
12
+ # @option options [String] :serial serial of the device run
13
+ # @option options [String] :bundle_id ID of the app to run
14
+ # @option options [Integer] :timeout Number of seconds before the debug session should be killed
15
+ # @return [Hash] Returns the stdout of the debug session
16
+ def self.run(options = {})
17
+ serial = options[:serial]
18
+ bundle_id = options[:bundle_id]
19
+ timeout = options[:timeout] || 10
20
+
21
+ result = execute("doalarm () { perl -e 'alarm shift; exec @ARGV' \"$@\"; }; doalarm #{timeout} idevicedebug -u #{serial} -d run #{bundle_id}")
22
+
23
+ raise IDeviceDebugError.new(result.stderr) unless [0, 255, 142].include?(result.exit)
24
+
25
+ result.stdout.split("\r\n")
26
+ end
27
+ end
28
+
29
+ # Error class for the IDeviceDebug class
30
+ class IDeviceDebugError < StandardError
31
+ def initialize(msg)
32
+ super(msg)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,96 @@
1
+ require 'device_api/execution'
2
+
3
+ # DeviceAPI - an interface to allow for automation of devices
4
+ module DeviceAPI
5
+ # iOS component of DeviceAPI
6
+ module IOS
7
+ # Namespace for all methods encapsulating idevice calls
8
+ class IDeviceInstaller < Execution
9
+ # Installs a given IPA to the specified device
10
+ # @param [Hash] options options for installing the app
11
+ # @option options [String] :ipa path to the IPA to install
12
+ # @option options [String] :serial serial of the target device
13
+ # @return [Boolean] true if successful, otherwise false
14
+ def self.install_ipa(options = {})
15
+ options[:action] = :install
16
+ change_package(options)
17
+ end
18
+
19
+ # Uninstalls a specified package from a device
20
+ # @param [Hash] options options for uninstalling the app
21
+ # @option options [String] :package bundle ID of the package to be uninstalled
22
+ # @option options [String] :serial serial of the target device
23
+ # @return [Boolean] true if successful, otherwise false
24
+ def self.uninstall_package(options = {})
25
+ options[:action] = :uninstall
26
+ change_package(options)
27
+ end
28
+
29
+ def self.change_package(options = {})
30
+ package = options[:package]
31
+ ipa = options[:ipa]
32
+ serial = options[:serial]
33
+ action = options[:action]
34
+
35
+ command = nil
36
+ if action == :install
37
+ command = "ideviceinstaller -u '#{serial}' -i '#{ipa}'"
38
+ elsif action == :uninstall
39
+ command = "ideviceinstaller -u '#{serial}' -U '#{package}'"
40
+ end
41
+
42
+ raise IDeviceInstallerError.new('No action specified') if command.nil?
43
+
44
+ result = execute(command)
45
+
46
+ raise IDeviceInstallerError.new(result.stderr) if result.exit != 0
47
+
48
+ lines = result.stdout.split("\n").map { |line| line.gsub('-', '').strip }
49
+
50
+ return true if lines.last.match('Complete')
51
+ false
52
+ end
53
+
54
+ # Lists packages installed on the specified device
55
+ # @param [String] serial serial of the target device
56
+ # @return [Hash] hash containing installed packages
57
+ def self.list_installed_packages(serial)
58
+ result = execute("ideviceinstaller -u '#{serial}' -l")
59
+
60
+ raise IDeviceInstallerError.new(result.stderr) if result.exit != 0
61
+
62
+ lines = result.stdout.split("\n")
63
+ lines.shift
64
+ packages = {}
65
+ lines.each do |line|
66
+ if /(.*)\s+-\s+(.*)\s+(\d.*)/.match(line)
67
+ packages[Regexp.last_match[2]] = { package_name: Regexp.last_match[1], version: Regexp.last_match[3] }
68
+ end
69
+ end
70
+ packages
71
+ end
72
+
73
+ # Check to see if a package is installed
74
+ # @param [Hash] options options for checking for installed package
75
+ # @option options [String] :package package ID to check for
76
+ # @option options [String] :serial serial of the target device
77
+ # @return [Boolean] true if the package is installed, false otherwise
78
+ def self.package_installed?(options = {})
79
+ package = options[:package]
80
+ serial = options[:serial]
81
+
82
+ installed_packages = list_installed_packages(serial)
83
+
84
+ matches = installed_packages.select { |_, values| values[:package_name] == package }
85
+ return !matches.empty?
86
+ end
87
+ end
88
+
89
+ # Error class for IDeviceInstaller class
90
+ class IDeviceInstallerError < StandardError
91
+ def initialize(msg)
92
+ super(msg)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,25 @@
1
+ # DeviceAPI - an interface to allow for automation of devices
2
+ module DeviceAPI
3
+ # iOS component of DeviceAPI
4
+ module IOS
5
+ # Namespace for all methods encapsulating idevicename calls
6
+ class IDeviceName < Execution
7
+
8
+ # Returns the device name based on the provided UUID
9
+ # @param device_id uuid of the device
10
+ # @return device name if device is connected
11
+ def self.name(device_id)
12
+ result = execute("idevicename -u #{device_id}")
13
+ return IDeviceNameError.new(result.stderr) if result.exit != 0
14
+ result.stdout.strip
15
+ end
16
+ end
17
+
18
+ # Error class for the IDeviceName class
19
+ class IDeviceNameError < StandardError
20
+ def initialize(msg)
21
+ super(msg)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,98 @@
1
+ # DeviceAPI - an interface to allow for automation of devices
2
+ module DeviceAPI
3
+ # iOS component of DeviceAPI
4
+ module IOS
5
+ # Namespace for all methods encapsulating ideviceprovision calls
6
+ class IDeviceProvision < Execution
7
+ # Lists all profiles on the specified device
8
+ # @param [String] serial serial of the device to check
9
+ # @return [Hash] hash of profile name and UUID
10
+ def self.list_profiles(serial)
11
+ result = execute("ideviceprovision -u #{serial} list")
12
+
13
+ raise IDeviceProvisionError.new(result.stderr) if result.exit != 0
14
+
15
+ Hash[result.stdout.split("\n").map { |a| b = a.split(' - '); [b[0], b[1]] }[1..-1]]
16
+ end
17
+
18
+ # Checks to see if a profile is installed on the specified device
19
+ # @param [Hash] options options used for checking profiles
20
+ # @option options [String] :name name of the profile (optional when uuid provided)
21
+ # @option options [String] :uuid UUID of the profile (optional when name provided)
22
+ # @option options [String] :serial serial of the device to check
23
+ # @return [Boolean] true if the profile is installed, false otherwise
24
+ def self.has_profile?(options = {})
25
+ name = options[:name]
26
+ uuid = options[:uuid]
27
+ serial = options[:serial]
28
+
29
+ profiles = list_profiles(serial)
30
+
31
+ profiles.key?(uuid) || profiles.value?(name)
32
+ end
33
+
34
+ # Removes the specified profile from the device
35
+ # @param [Hash] options options used for removing a profile
36
+ # @option options [String] :uuid UUID of the profile to be removed
37
+ # @option options [String] :serial serial of the device to remove the profile from
38
+ # @return [Boolean, IDeviceProvisionError] true if the profile is removed from the device, an error otherwise
39
+ def self.remove_profile(options = {})
40
+ uuid = options[:uuid]
41
+ serial = options[:serial]
42
+
43
+ return true unless has_profile?(serial: serial, uuid: uuid)
44
+
45
+ result = execute("ideviceprovision -u #{serial} remove #{uuid}")
46
+
47
+ raise IDeviceProvisionError.new(result.stderr) if result.exit != 0
48
+ true
49
+ end
50
+
51
+ # Installs the specified profile to the device
52
+ # @param [Hash] options options used for installing a profile
53
+ # @option options [String] :file path to the provisioning profile
54
+ # @option options [String] :serial serial of the device to install the profile to
55
+ # @return [Boolean, IDeviceProvisionError] true if the profile is installed, an error otherwise
56
+ def self.install_profile(options = {})
57
+ serial = options[:serial]
58
+ file = options[:file]
59
+
60
+ info = get_profile_info(file)
61
+
62
+ # Check to see if the profile has already been added to the device
63
+ return true if has_profile?(serial: serial, uuid: info['UUID'])
64
+
65
+ result = execute("ideviceprovision -u #{serial} install #{file}")
66
+
67
+ raise IDeviceProvisionError.new(result.stderr) if result.exit != 0
68
+ true
69
+ end
70
+
71
+ # Gets information about a provisioning profile
72
+ # @param [String] file path to the provisioning profile
73
+ # @return [Hash] hash containing provisioning profile information
74
+ def self.get_profile_info(file)
75
+ result = execute("ideviceprovision dump #{file}")
76
+
77
+ raise IDeviceProvisionError.new(result.stderr) if result.exit != 0
78
+
79
+ lines = result.stdout.split("\n")
80
+
81
+ info = {}
82
+ lines.each do |l|
83
+ if /(.*):\s+(.*)/.match(l)
84
+ info[Regexp.last_match[1]] = Regexp.last_match[2]
85
+ end
86
+ end
87
+ info
88
+ end
89
+ end
90
+
91
+ # Provisioning error class
92
+ class IDeviceProvisionError < StandardError
93
+ def initialize(msg)
94
+ super(msg)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,51 @@
1
+ require 'device_api/ios/idevicedebug'
2
+ require 'device_api/ios/ideviceinstaller'
3
+
4
+ # DeviceAPI - an interface to allow for automation of devices
5
+ module DeviceAPI
6
+ # iOS component of DeviceAPI
7
+ module IOS
8
+ # Namespace for all methods encapsulating idevice calls
9
+ class IPAddress < Execution
10
+
11
+ # Package name for the IP Address app
12
+ def self.ipaddress_bundle_id
13
+ 'uk.co.bbc.titan.IPAddress'
14
+ end
15
+
16
+ # Check to see if the IPAddress app is installed
17
+ # @param [String] serial serial of the target device
18
+ # @return [Boolean] returns true if the app is installed
19
+ def self.installed?(serial)
20
+ if DeviceAPI::IOS::IDeviceInstaller.package_installed?( serial: serial, package: ipaddress_bundle_id )
21
+ return true
22
+ else
23
+ raise IPAddressError.new('IP Address package not installed: Please see https://github.com/bbc/ios-test-helper')
24
+ end
25
+ end
26
+
27
+ # Get the IP Address from the installed app
28
+ # @param [String] serial serial of the target device
29
+ # @return [String] IP Address if found
30
+ def self.address(serial)
31
+ installed?(serial)
32
+ result = IDeviceDebug.run(serial: serial, bundle_id: ipaddress_bundle_id )
33
+
34
+ ip_address = nil
35
+ result.each do |line|
36
+ if /"en0\/ipv4" = "(.*)"/.match(line)
37
+ ip_address = Regexp.last_match[1]
38
+ end
39
+ end
40
+ ip_address
41
+ end
42
+ end
43
+
44
+ # Error class for the IPAddress class
45
+ class IPAddressError < StandardError
46
+ def initialize(msg)
47
+ super(msg)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,58 @@
1
+ require 'device_api/execution'
2
+ require 'device_api/ios/signing'
3
+ require 'ox'
4
+
5
+ # DeviceAPI - an interface to allow for automation of devices
6
+ module DeviceAPI
7
+ # iOS component of DeviceAPI
8
+ module IOS
9
+ # Namespace for all methods encapsulating plistutil calls
10
+ class Plistutil < Execution
11
+
12
+ # Check to ensure that plistutil is available
13
+ # @return [Boolean] true if plistutil is available, false otherwise
14
+ def self.plistutil_available?
15
+ result = execute('which plistutil')
16
+ result.exit == 0
17
+ end
18
+
19
+ # Gets properties from the IPA and returns them in a hash
20
+ # @param [String] path path to the IPA/App
21
+ # @return [Hash] list of properties from the app
22
+ def self.get_bundle_id_from_app(path)
23
+ path = Signing.unpack_ipa(path) if Signing.is_ipa?(path)
24
+ get_bundle_id_from_plist("#{path}/Info.plist")
25
+ end
26
+
27
+ # Gets properties from the IPA and returns them in a hash
28
+ # @param [String] plist path to the plist
29
+ # @return [Hash] list of properties from the app
30
+ def self.get_bundle_id_from_plist(plist)
31
+ raise PlistutilCommandError.new('plistutil not found') unless plistutil_available?
32
+ result = execute("plistutil -i #{plist}")
33
+ raise PlistutilCommandError.new(result.stderr) if result.exit != 0
34
+ info = Ox.parse(result.stdout)
35
+ nodes = info.locate('*/dict')
36
+ values = {}
37
+ last_key = nil
38
+ nodes.each do |node|
39
+ node.nodes.each do |child|
40
+ if child.value == 'key'
41
+ last_key = child.nodes.first
42
+ elsif child.value == 'string'
43
+ values[last_key] = child.nodes.first
44
+ end
45
+ end
46
+ end
47
+ values
48
+ end
49
+ end
50
+
51
+ # plistutil error class
52
+ class PlistutilCommandError < StandardError
53
+ def initialize(msg)
54
+ super(msg)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,86 @@
1
+ require 'device_api/execution'
2
+
3
+ # DeviceAPI - an interface to allow for automation of devices
4
+ module DeviceAPI
5
+ # iOS component of DeviceAPI
6
+ module IOS
7
+ # Namespace for all methods encapsulating idevice calls
8
+ class Signing < Execution
9
+
10
+ # Check to see if the path is an IPA
11
+ def self.is_ipa?(path)
12
+ return true if (File.extname path).downcase == '.ipa'
13
+ false
14
+ end
15
+
16
+ def self.unpack_ipa(path)
17
+ folder = File.dirname(path)
18
+ target = (File.basename path, (File.extname path))
19
+
20
+ # Check to see if the target has already been unzipped
21
+ return Dir["#{folder}/#{target}/Payload/*.app"].first if File.exists? ("#{folder}/#{target}/Payload/")
22
+
23
+ result = execute("unzip '#{path}' -d '#{folder}/#{target}'")
24
+ raise SigningCommandError.new(result.stderr) if result.exit != 0
25
+
26
+ Dir["#{folder}/#{target}/Payload/*.app"].first
27
+ end
28
+
29
+ def self.is_app_signed?(app_path)
30
+ app_path = unpack_ipa(app_path) if is_ipa?(app_path)
31
+ result = execute("codesign -d -vvvv '#{app_path}'")
32
+
33
+ if result.exit != 0
34
+ return false if /is not signed/.match(result.stderr)
35
+ raise SigningCommandError.new(result.stderr)
36
+ end
37
+
38
+ true
39
+ end
40
+
41
+ def self.sign_app(options = {})
42
+ cert = options[:cert]
43
+ entitlements = options[:entitlements]
44
+ app = options[:app]
45
+
46
+ result = execute("codesign --force --sign #{cert} --entitlements #{entitlements} '#{app}'")
47
+
48
+ raise SigningCommandError.new(result.stderr) if result.exit != 0
49
+
50
+ end
51
+
52
+ def self.get_signing_certs
53
+ result = execute('security find-identity -p codesigning -v')
54
+
55
+ raise SigningCommandError.new(result.stderr) if result.exit != 0
56
+
57
+ certs = []
58
+ result.stdout.split("\n").each do |line|
59
+ if /\)\s*(\S*)\s*"(.*)"/.match(line)
60
+ certs << { id: Regexp.last_match[1], name: Regexp.last_match[2] }
61
+ end
62
+ end
63
+ certs
64
+ end
65
+
66
+ def self.get_entitlements(app_path)
67
+ app_path = unpack_ipa(app_path) if is_ipa?(app_path)
68
+ result = execute("codesign -d --entitlements - #{app_path}")
69
+
70
+ require 'pry'
71
+ binding.pry
72
+ if result.exit != 0
73
+ raise SigningCommandError.new(result.stderr)
74
+ end
75
+ return result.stdout
76
+ end
77
+ end
78
+
79
+ # Signing error class
80
+ class SigningCommandError < StandardError
81
+ def initialize(msg)
82
+ super(msg)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,104 +1,51 @@
1
- $LOAD_PATH.unshift( './lib/' )
2
-
3
1
  require 'device_api/execution'
4
2
  require 'device_api/ios/idevice'
5
3
  require 'device_api/ios/device'
6
- require 'yaml'
4
+ require 'device_api/ios'
7
5
 
8
6
  include RSpec
9
7
 
10
- ProcessStatusStub = Struct.new(:exitstatus)
11
- $STATUS_ZERO = ProcessStatusStub.new(0)
12
-
13
8
  describe DeviceAPI::IOS do
14
9
 
15
- before(:all) do
16
-
17
- config_file = '../../config.yml'
18
- CONFIG = YAML.load_file(File.expand_path(config_file, __FILE__))
19
- @bundle_id = CONFIG["bundle_id"]
20
- @app_path = File.expand_path(CONFIG["app_path"])
21
- @arr_devices = DeviceAPI::IOS::IDevice.get_list_of_devices
22
- keys = @arr_devices[0].keys
23
- @test_device_uuid = keys[0]
24
- @app_name = CONFIG["app_name"]
25
-
26
- end
27
-
28
10
  describe ".model" do
29
11
 
30
- before(:each) do
31
- @ios_device = DeviceAPI::IOS::Device.new(:serial=>@test_device_uuid,:state => "ok",:type => "Device")
12
+ it 'returns the model of the attached device' do
13
+ device = DeviceAPI::IOS.device('123456')
14
+ expect(device.model).to eq('Unknown iOS device')
32
15
  end
33
16
 
34
- it "returns model of device" do
35
- props = DeviceAPI::IOS::IDevice.get_props(@test_device_uuid)
36
- expect(props['ModelNumber']).to eq(@ios_device.model)
17
+ it 'returns the correct result when a device is trusted' do
18
+ device = DeviceAPI::IOS.device('123456')
19
+ output = <<end
20
+ ActivationState: Activated
21
+ ActivationStateAcknowledged: true
22
+ end
23
+ allow(Open3).to receive(:capture3) {
24
+ [output, '', (Struct.new(:exitstatus)).new(0)]
25
+ }
26
+ expect(device.trusted?).to eq(true)
37
27
  end
38
28
 
39
-
40
- end
41
-
42
-
43
-
44
- describe ".install" do
45
-
46
- before(:each) do
47
-
48
- result = DeviceAPI::Execution.execute("ideviceinstaller -u #{@test_device_uuid} -l")
49
- if(result.stdout.include? @bundle_id and result.stderr=="")
50
- DeviceAPI::Execution.execute("ideviceinstaller -u '#{@test_device_uuid}' -U #{@bundle_id}")
51
- end
52
-
53
- @ios_device = DeviceAPI::IOS::Device.new(:serial=>@test_device_uuid,:state => "ok",:type => "Device")
54
-
29
+ it 'returns the correct result when a device is not trusted' do
30
+ device = DeviceAPI::IOS.device('123456')
31
+ expect(device.trusted?).to eq(false)
55
32
  end
56
33
 
57
- it "returns successfully installed message once ios app is installed on a device" do
58
- expect(@ios_device.install(@app_path)).to eq :success
34
+ it 'returns device state' do
35
+ device = DeviceAPI::IOS.device('123456')
36
+ expect(device.status).to eq(:ok)
59
37
  end
60
-
61
38
 
62
- end
63
-
64
- describe ".uninstall" do
65
-
66
- before(:each) do
67
- @ios_device = DeviceAPI::IOS::Device.new(:serial=>@test_device_uuid,:state => "ok",:type => "Device")
68
- app_bundle_list = @ios_device.bundle_id_list
69
- app_names = app_bundle_list.keys
70
- result = DeviceAPI::Execution.execute("ideviceinstaller -u #{@test_device_uuid} -l")
71
-
72
- if(!app_names.include? @app_name)
73
- DeviceAPI::Execution.execute("ideviceinstaller -u '#{@test_device_uuid}' -i #{@app_path}")
74
- @local_bundle_id = (@ios_device.bundle_id_list)[@app_name]
75
- else
76
- @local_bundle_id = app_bundle_list[@app_name]
77
- end
78
- end
79
-
80
- it "Uninstalls the app from ios device" do
81
- res = @ios_device.uninstall(@local_bundle_id)
82
- expect( res ).to eq :success
83
-
39
+ it 'returns the device name' do
40
+ output = <<-end
41
+ Test Device
42
+ end
43
+ allow(Open3).to receive(:capture3) {
44
+ [output, '', (Struct.new(:exitstatus)).new(0)]
45
+ }
46
+ device = DeviceAPI::IOS.device('123456')
47
+ expect(device.name).to eq('Test Device')
84
48
  end
85
-
86
-
87
49
  end
88
-
89
- describe ".get_props" do
90
- before(:each) do
91
- @ios_device = DeviceAPI::IOS::Device.new(:serial=>@test_device_uuid,:state => "ok",:type => "Device")
92
- end
93
-
94
- it "Returns a hash of name value pair properties" do
95
- props = @ios_device.get_props
96
- expect( props ).to be_a Hash
97
- expect( props['ActivationState']).to eq('Activated')
98
- end
99
-
100
- end
101
-
102
-
103
50
  end
104
51
 
@@ -0,0 +1,97 @@
1
+ require 'device_api/ios/ideviceinstaller'
2
+
3
+ describe DeviceAPI::IOS::IDeviceInstaller do
4
+ describe '.list_installed_packages' do
5
+ it 'returns a list of installed apps' do
6
+ output = <<end
7
+ Total: 2 apps
8
+ uk.co.bbc.titan.IPAddress - IPAddress 1
9
+ uk.co.bbc.iplayer - BBC iPlayer 4.10.0.196
10
+ end
11
+ allow(Open3).to receive(:capture3) do
12
+ [output, '', (Struct.new(:exitstatus)).new(0)]
13
+ end
14
+ apps = DeviceAPI::IOS::IDeviceInstaller.list_installed_packages('123456')
15
+ expect(apps.count).to eq(2)
16
+ end
17
+ end
18
+
19
+ describe '.install_ipa' do
20
+ it 'installs an app' do
21
+ output = <<end
22
+ Installing 'uk.co.mediaat.iplayer'
23
+ - CreatingStagingDirectory (5%)
24
+ - ExtractingPackage (15%)
25
+ - InspectingPackage (20%)
26
+ - TakingInstallLock (20%)
27
+ - PreflightingApplication (30%)
28
+ - InstallingEmbeddedProfile (30%)
29
+ - VerifyingApplication (40%)
30
+ - CreatingContainer (50%)
31
+ - InstallingApplication (60%)
32
+ - PostflightingApplication (70%)
33
+ - SandboxingApplication (80%)
34
+ - GeneratingApplicationMap (90%)
35
+ - Complete
36
+ end
37
+ allow(Open3).to receive(:capture3) do
38
+ [output, '', (Struct.new(:exitstatus)).new(0)]
39
+ end
40
+ result = DeviceAPI::IOS::IDeviceInstaller.install_ipa(serial: '123456', ipa: 'iplayer.ipa' )
41
+ expect(result).to eq(true)
42
+ end
43
+ end
44
+
45
+ describe '.uninstall_app' do
46
+ it 'uninstalls an app' do
47
+ output = <<end
48
+ Uninstalling 'uk.co.bbc.iplayer'
49
+ - RemovingApplication (50%)
50
+ - GeneratingApplicationMap (90%)
51
+ - Complete
52
+ end
53
+ allow(Open3).to receive(:capture3) do
54
+ [output, '', (Struct.new(:exitstatus)).new(0)]
55
+ end
56
+ result = DeviceAPI::IOS::IDeviceInstaller.uninstall_package(package: 'uk.co.bbc.iplayer', serial: '123456')
57
+ expect(result).to eq(true)
58
+ end
59
+
60
+ it 'fails to remove an app that is not installed' do
61
+ output = <<end
62
+ Uninstalling 'uk.co.bbc.iplaye'
63
+ - RemovingApplication (50%)
64
+ - GeneratingApplicationMap (90%)
65
+ - Error occurred: APIInternalError
66
+ end
67
+ allow(Open3).to receive(:capture3) do
68
+ [output, '', (Struct.new(:exitstatus)).new(0)]
69
+ end
70
+ result = DeviceAPI::IOS::IDeviceInstaller.uninstall_package(package: 'uk.co.bbc.iplaye', serial: '123456')
71
+ expect(result).to eq(false)
72
+ end
73
+ end
74
+
75
+ describe '.package_installed?' do
76
+ before(:each) do
77
+ output = <<end
78
+ Total: 2 apps
79
+ uk.co.bbc.titan.IPAddress - IPAddress 1
80
+ uk.co.bbc.iplayer - BBC iPlayer 4.10.0.196
81
+ end
82
+ allow(Open3).to receive(:capture3) do
83
+ [output, '', (Struct.new(:exitstatus)).new(0)]
84
+ end
85
+ end
86
+
87
+ it 'identifies when a package is installed' do
88
+ result = DeviceAPI::IOS::IDeviceInstaller.package_installed?(package: 'uk.co.bbc.iplayer', serial: '123456')
89
+ expect(result).to eq(true)
90
+ end
91
+
92
+ it 'identifies when a package is not installed' do
93
+ result = DeviceAPI::IOS::IDeviceInstaller.package_installed?(package: 'uk.co.bbc.sport', serial: '123456')
94
+ expect(result).to eq(false)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,35 @@
1
+ require 'device_api/execution'
2
+ require 'device_api/ios/ipaddress'
3
+
4
+ describe DeviceAPI::IOS::IPAddress do
5
+
6
+ describe '.address' do
7
+ it 'gets the correct IP Address' do
8
+
9
+ apps = <<end
10
+ Total: 2 apps\r
11
+ uk.co.bbc.titan.IPAddress - IPAddress 1\r
12
+ uk.co.bbc.iplayer - BBC iPlayer 4.10.0.196\r
13
+ end
14
+
15
+ output = <<end
16
+ 2015-07-06 09:27:33.670 IPAddress[801:299377] addresses: {
17
+ "awdl0/ipv6" = "fe80::dce9:ge0f:fee7:aaad";
18
+ "en0/ipv4" = "10.10.1.80";
19
+ "en0/ipv6" = "fe80::147c:1496:fce9:ed2";
20
+ "lo0/ipv4" = "127.0.0.1";
21
+ "lo0/ipv6" = "fe80::1";
22
+ "pdp_ip0/ipv4" = "10.7.195.3";
23
+ }
24
+ 2015-07-06 09:27:33.672 IPAddress[801:299377] 10.10.1.80
25
+ end
26
+ allow(Open3).to receive(:capture3).and_return(
27
+ [apps, '', (Struct.new(:exitstatus)).new(0)],
28
+ [output, '', (Struct.new(:exitstatus)).new(0)]
29
+ )
30
+
31
+ ip_address = DeviceAPI::IOS::IPAddress.address('123456')
32
+ expect(ip_address).to eq('10.10.1.80')
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ require 'device_api/ios/plistutil'
2
+
3
+ describe DeviceAPI::IOS::Plistutil do
4
+ describe ".get_app_bundle_id" do
5
+ it "returns the correct app bundle" do
6
+
7
+ xml = <<end
8
+ <?xml version="1.0" encoding="UTF-8"?>
9
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
10
+ <plist version="1.0">
11
+ <dict>
12
+ <key>CFBundleIdentifier</key>
13
+ <string>com.example.apple-samplecode.UICatalog</string>
14
+ </dict>
15
+ </plist>
16
+ end
17
+
18
+ allow(Open3).to receive(:capture3) {
19
+ [xml, '', (Struct.new(:exitstatus)).new(0)]
20
+ }
21
+ bundle = DeviceAPI::IOS::Plistutil.get_bundle_id_from_plist('Info.plist')
22
+ expect(bundle['CFBundleIdentifier']).to eq('com.example.apple-samplecode.UICatalog')
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,62 @@
1
+ $LOAD_PATH.unshift( './lib/' )
2
+
3
+ require 'device_api/execution'
4
+ require 'device_api/ios/signing'
5
+ require 'yaml'
6
+
7
+ include RSpec
8
+
9
+ describe DeviceAPI::IOS::Signing do
10
+
11
+ before(:all) do
12
+
13
+ end
14
+
15
+ describe ".is_ipa?" do
16
+ it "correctly identifies an IPA" do
17
+ expect(DeviceAPI::IOS::Signing.is_ipa?('/path/to/ipa.ipa')).to be(true)
18
+ end
19
+
20
+ it "correctly identifies a file that isn't an IPA" do
21
+ expect(DeviceAPI::IOS::Signing.is_ipa?('/path/to/app.app')).to be(false)
22
+ end
23
+ end
24
+
25
+ describe ".get_signing_certs" do
26
+ it "returns an Array of Hashes containing iOS Signing Certificates" do
27
+ expect(DeviceAPI::IOS::Signing.get_signing_certs).to be_kind_of(Array)
28
+ end
29
+
30
+ it "returns an Array of Hashes containing correct certificates" do
31
+ out = <<-eos
32
+ 1) 43ED4FA24518B1F72EE4FB3E6F7476E886A8E5D0 "iPhone Developer: Test Developer (ABC1234567)"
33
+ 2) 289765876A0FB55327F8F3C2A3D4FA3F1A484CFB "iPhone Developer: Test Developer (ABC1234526)"
34
+ 3) 132128763516473546816751267AFA217036217B "iPhone Developer: Test Developer (ABC1235343)"
35
+ 3 valid identities found
36
+ eos
37
+ allow(Open3).to receive(:capture3) {
38
+ [out, '', (Struct.new(:exitstatus)).new(0)]
39
+ }
40
+
41
+ expect(DeviceAPI::IOS::Signing.get_signing_certs.count).to eq(3)
42
+ end
43
+ end
44
+
45
+ describe "entitlements" do
46
+ it 'returns a list of entitlements for an app' do
47
+ out = <<-eos
48
+ <dict>
49
+ <key>application-identifier</key>
50
+ <string>uk.co.bbc.test</string>
51
+ <key>get-task-allow</key>
52
+ <false/>
53
+ </dict>
54
+ eos
55
+ allow(Open3).to receive(:capture3) {
56
+ [out, '', (Struct.new(:exitstatus)).new(0)]
57
+ }
58
+
59
+ expect(DeviceAPI::IOS::Signing.get_entitlements('test.ipa')).to eq(true)
60
+ end
61
+ end
62
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: device_api-ios
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - BBC
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2015-05-22 00:00:00.000000000 Z
13
+ date: 2016-02-03 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: device_api
@@ -32,6 +32,34 @@ dependencies:
32
32
  - - "<"
33
33
  - !ruby/object:Gem::Version
34
34
  version: '2.0'
35
+ - !ruby/object:Gem::Dependency
36
+ name: ios-devices
37
+ requirement: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0.2'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0.2'
49
+ - !ruby/object:Gem::Dependency
50
+ name: ox
51
+ requirement: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 2.1.0
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.1.0
35
63
  - !ruby/object:Gem::Dependency
36
64
  name: rspec
37
65
  requirement: !ruby/object:Gem::Requirement
@@ -62,8 +90,18 @@ files:
62
90
  - lib/device_api/ios.rb
63
91
  - lib/device_api/ios/device.rb
64
92
  - lib/device_api/ios/idevice.rb
93
+ - lib/device_api/ios/idevicedebug.rb
94
+ - lib/device_api/ios/ideviceinstaller.rb
95
+ - lib/device_api/ios/idevicename.rb
96
+ - lib/device_api/ios/ideviceprovision.rb
97
+ - lib/device_api/ios/ipaddress.rb
98
+ - lib/device_api/ios/plistutil.rb
99
+ - lib/device_api/ios/signing.rb
65
100
  - spec/ios_device_spec.rb
66
- - spec/ios_idevice_spec.rb
101
+ - spec/ios_ideviceinstaller_spec.rb
102
+ - spec/ios_ipaddress_spec.rb
103
+ - spec/ios_plistutil_spec.rb
104
+ - spec/ios_signing_spec.rb
67
105
  homepage: https://github.com/bbc/device_api-ios
68
106
  licenses:
69
107
  - MIT
@@ -84,9 +122,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
122
  version: '0'
85
123
  requirements: []
86
124
  rubyforge_project:
87
- rubygems_version: 2.4.5
125
+ rubygems_version: 2.4.8
88
126
  signing_key:
89
127
  specification_version: 4
90
128
  summary: IOS Device Management API
91
129
  test_files: []
92
- has_rdoc:
@@ -1,109 +0,0 @@
1
- $LOAD_PATH.unshift( './lib/' )
2
-
3
- require 'device_api/execution'
4
- require 'device_api/ios/idevice'
5
- require 'yaml'
6
-
7
- include RSpec
8
-
9
- ProcessStatusStub = Struct.new(:exitstatus)
10
- $STATUS_ZERO = ProcessStatusStub.new(0)
11
-
12
- describe DeviceAPI::IOS::IDevice do
13
-
14
- before(:all) do
15
-
16
- config_file = '../../config.yml'
17
- CONFIG = YAML.load_file(File.expand_path(config_file, __FILE__))
18
- @bundle_id = CONFIG["bundle_id"]
19
- @app_path = File.expand_path(CONFIG["app_path"])
20
- @arr_devices = DeviceAPI::IOS::IDevice.get_list_of_devices
21
- keys = @arr_devices[0].keys
22
- @test_device_uuid = keys[0]
23
-
24
- end
25
-
26
- describe ".get_list_of_devices" do
27
-
28
- before(:each) do
29
- @arr_devices = DeviceAPI::IOS::IDevice.get_list_of_devices
30
- end
31
-
32
- it "returns an Array of connected ios devices" do
33
- expect(@arr_devices.class).to eq(Array)
34
- end
35
-
36
- it "returns an Array of Hash of connected ios devices - When atleast one devices is connected" do
37
- expect(@arr_devices[0].class).to eq(Hash)
38
- end
39
-
40
- end
41
-
42
- describe ".get_state" do
43
-
44
- it "returns state of a device based on the device - When atleast one device is connected" do
45
- expect( DeviceAPI::IOS::IDevice.get_state(@test_device_uuid)).to eq("Activated")
46
- end
47
-
48
-
49
- end
50
-
51
- describe ".install_app" do
52
-
53
- before(:each) do
54
-
55
- result = DeviceAPI::Execution.execute("ideviceinstaller -u #{@test_device_uuid} -l")
56
- if(result.stdout.include? @bundle_id and result.stderr=="")
57
- DeviceAPI::Execution.execute("ideviceinstaller -u '#{@test_device_uuid}' -U #{@bundle_id}")
58
- end
59
-
60
- end
61
-
62
- it "returns successfully installed message once ios app is installed on a device" do
63
- expect( DeviceAPI::IOS::IDevice.install_app(@test_device_uuid,@app_path)).to eq true
64
- end
65
-
66
-
67
- end
68
-
69
- describe ".launch_app" do
70
-
71
- before(:each) do
72
-
73
- result = DeviceAPI::Execution.execute("ideviceinstaller -u #{@test_device_uuid} -l")
74
- if(result.stdout.include? @bundle_id and result.stderr=="")
75
- DeviceAPI::Execution.execute("ideviceinstaller -u '#{@test_device_uuid}' -U #{@bundle_id}")
76
- end
77
-
78
- end
79
-
80
- it "Launches the app on the ios device" do
81
- props = DeviceAPI::IOS::IDevice.launch_app(@test_device_uuid,@app_path)
82
- expect( props ).to eq true
83
-
84
- end
85
-
86
-
87
- end
88
-
89
- describe ".get_props" do
90
-
91
- it "Returns a hash of name value pair properties" do
92
- props = DeviceAPI::IOS::IDevice.get_props(@test_device_uuid)
93
- expect( props ).to be_a Hash
94
- expect( props['ActivationState']).to eq('Activated')
95
- end
96
-
97
- end
98
-
99
- describe ".bundle_id_list" do
100
-
101
- it "Returns a hash of name value pair of application name and bundle_id" do
102
- list = DeviceAPI::IOS::IDevice.bundle_id_list(@test_device_uuid)
103
- expect(list).to be_a Hash
104
- end
105
-
106
- end
107
-
108
- end
109
-