device_api-ios 1.0.1 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
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
-