spaceship 0.0.15 → 0.1.0

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/assets/languageMapping.json +224 -0
  3. data/lib/spaceship.rb +20 -63
  4. data/lib/spaceship/base.rb +71 -14
  5. data/lib/spaceship/client.rb +9 -274
  6. data/lib/spaceship/launcher.rb +1 -1
  7. data/lib/spaceship/portal/app.rb +125 -0
  8. data/lib/spaceship/portal/certificate.rb +273 -0
  9. data/lib/spaceship/portal/device.rb +102 -0
  10. data/lib/spaceship/portal/portal.rb +6 -0
  11. data/lib/spaceship/portal/portal_base.rb +13 -0
  12. data/lib/spaceship/portal/portal_client.rb +289 -0
  13. data/lib/spaceship/portal/provisioning_profile.rb +369 -0
  14. data/lib/spaceship/portal/spaceship.rb +94 -0
  15. data/lib/spaceship/{ui → portal/ui}/select_team.rb +0 -0
  16. data/lib/spaceship/tunes/app_screenshot.rb +28 -0
  17. data/lib/spaceship/tunes/app_status.rb +63 -0
  18. data/lib/spaceship/tunes/app_submission.rb +149 -0
  19. data/lib/spaceship/tunes/app_version.rb +337 -0
  20. data/lib/spaceship/tunes/application.rb +253 -0
  21. data/lib/spaceship/tunes/build.rb +128 -0
  22. data/lib/spaceship/tunes/build_train.rb +79 -0
  23. data/lib/spaceship/tunes/language_converter.rb +44 -0
  24. data/lib/spaceship/tunes/language_item.rb +54 -0
  25. data/lib/spaceship/tunes/processing_build.rb +30 -0
  26. data/lib/spaceship/tunes/spaceship.rb +26 -0
  27. data/lib/spaceship/tunes/tester.rb +177 -0
  28. data/lib/spaceship/tunes/tunes.rb +12 -0
  29. data/lib/spaceship/tunes/tunes_base.rb +15 -0
  30. data/lib/spaceship/tunes/tunes_client.rb +360 -0
  31. data/lib/spaceship/version.rb +1 -1
  32. metadata +27 -7
  33. data/lib/spaceship/app.rb +0 -125
  34. data/lib/spaceship/certificate.rb +0 -271
  35. data/lib/spaceship/device.rb +0 -100
  36. data/lib/spaceship/provisioning_profile.rb +0 -367
@@ -0,0 +1,44 @@
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
+ private
18
+ # Path to the gem to fetch resoures
19
+ def spaceship_gem_path
20
+ if Gem::Specification::find_all_by_name('spaceship').any?
21
+ return Gem::Specification.find_by_name('spaceship').gem_dir
22
+ else
23
+ return './'
24
+ end
25
+ end
26
+
27
+ # Get the mapping JSON parsed
28
+ def mapping
29
+ @languages ||= JSON.parse(File.read(File.join(spaceship_gem_path, "lib", "assets", "languageMapping.json")))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ class String
37
+ def to_language_code
38
+ Spaceship::Tunes::LanguageConverter.from_itc_to_standard(self)
39
+ end
40
+
41
+ def to_full_language
42
+ Spaceship::Tunes::LanguageConverter.from_standard_to_itc(self)
43
+ end
44
+ end
@@ -0,0 +1,54 @@
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
+ self.identifier = identifier.to_s
10
+ self.original_array = ref
11
+ end
12
+
13
+ def [](key)
14
+ get_lang(key)[identifier]['value']
15
+ end
16
+
17
+ def []=(key, value)
18
+ get_lang(key)[identifier]['value'] = value
19
+ end
20
+
21
+ def get_lang(lang)
22
+ result = self.original_array.find do |current|
23
+ current['language'] == lang
24
+ end
25
+ return result if result
26
+
27
+ raise "Language '#{lang}' is not activated for this app version."
28
+ end
29
+
30
+ # @return (Array) An array containing all languages that are already available
31
+ def keys
32
+ self.original_array.collect { |l| l.fetch('language') }
33
+ end
34
+
35
+ # @return (Array) An array containing all languages that are already available
36
+ # alias for keys
37
+ def languages
38
+ keys
39
+ end
40
+
41
+ def inspect
42
+ result = ""
43
+ self.original_array.collect do |current|
44
+ result += "#{current.fetch('language')}: #{current.fetch(identifier, {}).fetch('value')}\n"
45
+ end
46
+ result
47
+ end
48
+
49
+ def to_s
50
+ inspect
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,30 @@
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
+
6
+ # @return [String] The state of this build
7
+ # @example
8
+ # ITC.apps.betaProcessingStatus.InvalidBinary
9
+ # @example
10
+ # ITC.apps.betaProcessingStatus.Created
11
+ # @example
12
+ # ITC.apps.betaProcessingStatus.Uploaded
13
+ attr_accessor :state
14
+
15
+ # @return (Integer) The number of ticks since 1970 (e.g. 1413966436000)
16
+ attr_accessor :upload_date
17
+
18
+ attr_mapping(
19
+ 'state' => :state,
20
+ 'uploadDate' => :upload_date
21
+ )
22
+
23
+ class << self
24
+ def factory(attrs)
25
+ self.new(attrs)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
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
+ end
25
+ end
26
+ end
@@ -0,0 +1,177 @@
1
+ module Spaceship
2
+ module Tunes
3
+ class Tester < TunesBase
4
+
5
+ # @return (String) The identifier of this tester, provided by iTunes Connect
6
+ # @example
7
+ # "60f858b4-60a8-428a-963a-f943a3d68d17"
8
+ attr_accessor :tester_id
9
+
10
+ # @return (String) The email of this tester
11
+ # @example
12
+ # "tester@spaceship.com"
13
+ attr_accessor :email
14
+
15
+ # @return (String) The first name of this tester
16
+ # @example
17
+ # "Cary"
18
+ attr_accessor :first_name
19
+
20
+ # @return (String) The last name of this tester
21
+ # @example
22
+ # "Bennett"
23
+ attr_accessor :last_name
24
+
25
+ # @return (Array) An array of registered devices for this user
26
+ # @example
27
+ # [{
28
+ # "model": "iPhone 6",
29
+ # "os": "iOS",
30
+ # "osVersion": "8.3",
31
+ # "name": null
32
+ # }]
33
+ attr_accessor :devices
34
+
35
+ attr_mapping(
36
+ 'testerId' => :tester_id,
37
+ 'emailAddress.value' => :email,
38
+ 'firstName.value' => :first_name,
39
+ 'lastName.value' => :last_name,
40
+ 'devices' => :devices
41
+ )
42
+
43
+ class << self
44
+
45
+ # @return (Hash) All urls for the ITC used for web requests
46
+ def url
47
+ raise "You have to use a subclass: Internal or External"
48
+ end
49
+
50
+ # Create a new object based on a hash.
51
+ # This is used to create a new object based on the server response.
52
+ def factory(attrs)
53
+ self.new(attrs)
54
+ end
55
+
56
+ # @return (Array) Returns all beta testers available for this account
57
+ def all
58
+ client.testers(self).map { |tester| self.factory(tester) }
59
+ end
60
+
61
+ # @return (Spaceship::Tunes::Tester) Returns the tester matching the parameter
62
+ # as either the Tester id or email
63
+ # @param identifier (String) (required): Value used to filter the tester
64
+ def find(identifier)
65
+ all.find do |tester|
66
+ (tester.tester_id == identifier.to_s or tester.email == identifier)
67
+ end
68
+ end
69
+
70
+ # Create new tester in iTunes Connect
71
+ # @param email (String) (required): The email of the new tester
72
+ # @param first_name (String) (optional): The first name of the new tester
73
+ # @param last_name (String) (optional): The last name of the new tester
74
+ # @example
75
+ # Spaceship::Tunes::Tester.external.create!(email: "tester@mathiascarignani.com", first_name: "Cary", last_name:"Bennett")
76
+ # @return (Tester): The newly created tester
77
+ def create!(email: nil, first_name: nil, last_name: nil)
78
+ data = client.create_tester!(tester: self,
79
+ email: email,
80
+ first_name: first_name,
81
+ last_name: last_name)
82
+ self.factory(data)
83
+ end
84
+
85
+ #####################################################
86
+ # @!group App
87
+ #####################################################
88
+
89
+ # @return (Array) Returns all beta testers available for this account filtered by app
90
+ # @param app_id (String) (required): The app id to filter the testers
91
+ def all_by_app(app_id)
92
+ client.testers_by_app(self, app_id).map { |tester| self.factory(tester) }
93
+ end
94
+
95
+ # @return (Spaceship::Tunes::Tester) Returns the tester matching the parameter
96
+ # as either the Tester id or email
97
+ # @param app_id (String) (required): The app id to filter the testers
98
+ # @param identifier (String) (required): Value used to filter the tester
99
+ def find_by_app(app_id, identifier)
100
+ all_by_app(app_id).find do |tester|
101
+ (tester.tester_id == identifier.to_s or tester.email == identifier)
102
+ end
103
+ end
104
+
105
+ # Add all testers to the app received
106
+ # @param app_id (String) (required): The app id to filter the testers
107
+ def add_all_to_app!(app_id)
108
+ # TODO: Change to not make one request for each tester
109
+ all.each do |tester|
110
+ begin
111
+ tester.add_to_app!(app_id)
112
+ rescue => ex
113
+ if ex.to_s.include?"testerEmailExistsInternal" or ex.to_s.include?"duplicate.email"
114
+ # That's a non-relevant error message by iTC
115
+ # ignore that
116
+ else
117
+ raise ex
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ def setup
125
+ self.devices ||= [] # by default, an empty array instead of nil
126
+ end
127
+
128
+ #####################################################
129
+ # @!group Subclasses
130
+ #####################################################
131
+ class External < Tester
132
+ def self.url(app_id = nil)
133
+ {
134
+ index: "ra/users/pre/ext",
135
+ index_by_app: "ra/user/externalTesters/#{app_id}/",
136
+ create: "ra/users/pre/create",
137
+ delete: "ra/users/pre/ext/delete",
138
+ update_by_app: "ra/user/externalTesters/#{app_id}/"
139
+ }
140
+ end
141
+ end
142
+
143
+ class Internal < Tester
144
+ def self.url(app_id = nil)
145
+ {
146
+ index: "ra/users/pre/int",
147
+ index_by_app: "ra/user/internalTesters/#{app_id}/",
148
+ create: nil,
149
+ delete: nil,
150
+ update_by_app: "ra/user/internalTesters/#{app_id}/"
151
+ }
152
+ end
153
+ end
154
+
155
+ # Delete current tester
156
+ def delete!
157
+ client.delete_tester!(self)
158
+ end
159
+
160
+ #####################################################
161
+ # @!group App
162
+ #####################################################
163
+
164
+ # Add current tester to list of the app testers
165
+ # @param app_id (String) (required): The id of the application to which want to modify the list
166
+ def add_to_app!(app_id)
167
+ client.add_tester_to_app!(self, app_id)
168
+ end
169
+
170
+ # Remove current tester from list of the app testers
171
+ # @param app_id (String) (required): The id of the application to which want to modify the list
172
+ def remove_from_app!(app_id)
173
+ client.remove_tester_from_app!(self, app_id)
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,12 @@
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_screenshot'
9
+ require 'spaceship/tunes/language_converter'
10
+ require 'spaceship/tunes/build'
11
+ require 'spaceship/tunes/processing_build'
12
+ require 'spaceship/tunes/build_train'
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,360 @@
1
+ module Spaceship
2
+ class TunesClient < Spaceship::Client
3
+
4
+ #####################################################
5
+ # @!group Init and Login
6
+ #####################################################
7
+
8
+ def self.hostname
9
+ "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/"
10
+ end
11
+
12
+ # Fetches the latest login URL from iTunes Connect
13
+ def login_url
14
+ cache_path = "/tmp/spaceship_itc_login_url.txt"
15
+ begin
16
+ cached = File.read(cache_path)
17
+ rescue Errno::ENOENT
18
+ end
19
+ return cached if cached
20
+
21
+ host = "https://itunesconnect.apple.com"
22
+ begin
23
+ url = host + request(:get, self.class.hostname).body.match(/action="(\/WebObjects\/iTunesConnect.woa\/wo\/.*)"/)[1]
24
+ raise "" unless url.length > 0
25
+
26
+ File.write(cache_path, url) # TODO
27
+ return url
28
+ rescue => ex
29
+ puts ex
30
+ raise "Could not fetch the login URL from iTunes Connect, the server might be down"
31
+ end
32
+ end
33
+
34
+ def send_login_request(user, password)
35
+ response = request(:post, login_url, {
36
+ theAccountName: user,
37
+ theAccountPW: password
38
+ })
39
+
40
+ if response['Set-Cookie'] =~ /myacinfo=(\w+);/
41
+ # To use the session properly we'll need the following cookies:
42
+ # - myacinfo
43
+ # - woinst
44
+ # - wosid
45
+
46
+ begin
47
+ cooks = response['Set-Cookie']
48
+
49
+ to_use = [
50
+ "myacinfo=" + cooks.match(/myacinfo=(\w+)/)[1],
51
+ "woinst=" + cooks.match(/woinst=(\w+)/)[1],
52
+ "wosid=" + cooks.match(/wosid=(\w+)/)[1]
53
+ ]
54
+
55
+ @cookie = to_use.join(';')
56
+ rescue => ex
57
+ # User Credentials are wrong
58
+ raise InvalidUserCredentialsError.new(response)
59
+ end
60
+
61
+ return @client
62
+ else
63
+ # User Credentials are wrong
64
+ raise InvalidUserCredentialsError.new(response)
65
+ end
66
+ end
67
+
68
+ def handle_itc_response(data)
69
+ return unless data
70
+ return unless data.kind_of?Hash
71
+
72
+ if data.fetch('sectionErrorKeys', []).count == 0 and
73
+ data.fetch('sectionInfoKeys', []).count == 0 and
74
+ data.fetch('sectionWarningKeys', []).count == 0
75
+
76
+ logger.debug("Request was successful")
77
+ end
78
+
79
+ def handle_response_hash(hash)
80
+ errors = []
81
+ if hash.kind_of?Hash
82
+ hash.each do |key, value|
83
+ errors = errors + handle_response_hash(value)
84
+
85
+ if key == 'errorKeys' and value.kind_of?Array and value.count > 0
86
+ errors = errors + value
87
+ end
88
+ end
89
+ elsif hash.kind_of?Array
90
+ hash.each do |value|
91
+ errors = errors + handle_response_hash(value)
92
+ end
93
+ else
94
+ # We don't care about simple values
95
+ end
96
+ return errors
97
+ end
98
+
99
+ errors = handle_response_hash(data)
100
+ errors = errors + data.fetch('sectionErrorKeys') if data['sectionErrorKeys']
101
+
102
+ # Sometimes there is a different kind of error in the JSON response
103
+ different_error = data.fetch('messages', {}).fetch('error', nil)
104
+ errors << different_error if different_error
105
+
106
+ raise errors.join(' ') if errors.count > 0 # they are separated by `.` by default
107
+
108
+ puts data['sectionInfoKeys'] if data['sectionInfoKeys']
109
+ puts data['sectionWarningKeys'] if data['sectionWarningKeys']
110
+
111
+ return data
112
+ end
113
+
114
+
115
+ #####################################################
116
+ # @!group Applications
117
+ #####################################################
118
+
119
+ def applications
120
+ r = request(:get, 'ra/apps/manageyourapps/summary')
121
+ parse_response(r, 'data')['summaries']
122
+ end
123
+
124
+ # Creates a new application on iTunes Connect
125
+ # @param name (String): The name of your app as it will appear on the App Store.
126
+ # This can't be longer than 255 characters.
127
+ # @param primary_language (String): If localized app information isn't available in an
128
+ # App Store territory, the information from your primary language will be used instead.
129
+ # @param version (String): The version number is shown on the App Store and should
130
+ # match the one you used in Xcode.
131
+ # @param sku (String): A unique ID for your app that is not visible on the App Store.
132
+ # @param bundle_id (String): The bundle ID must match the one you used in Xcode. It
133
+ # can't be changed after you submit your first build.
134
+ def create_application!(name: nil, primary_language: nil, version: nil, sku: nil, bundle_id: nil, bundle_id_suffix: nil)
135
+ # First, we need to fetch the data from Apple, which we then modify with the user's values
136
+ r = request(:get, 'ra/apps/create/?appType=ios')
137
+ data = parse_response(r, 'data')
138
+
139
+ # Now fill in the values we have
140
+ data['versionString']['value'] = version
141
+ data['newApp']['name']['value'] = name
142
+ data['newApp']['bundleId']['value'] = bundle_id
143
+ data['newApp']['primaryLanguage']['value'] = primary_language || 'English_CA'
144
+ data['newApp']['vendorId']['value'] = sku
145
+ data['newApp']['bundleIdSuffix']['value'] = bundle_id_suffix
146
+
147
+ # Now send back the modified hash
148
+ r = request(:post) do |req|
149
+ req.url 'ra/apps/create/?appType=ios'
150
+ req.body = data.to_json
151
+ req.headers['Content-Type'] = 'application/json'
152
+ end
153
+
154
+ data = parse_response(r, 'data')
155
+ handle_itc_response(data)
156
+ end
157
+
158
+ def create_version!(app_id, version_number)
159
+ r = request(:post) do |req|
160
+ req.url "ra/apps/version/create/#{app_id}"
161
+ req.body = { version: version_number.to_s }.to_json
162
+ req.headers['Content-Type'] = 'application/json'
163
+ end
164
+
165
+ parse_response(r, 'data')
166
+ end
167
+
168
+ def get_resolution_center(app_id)
169
+ r = request(:get, "ra/apps/#{app_id}/resolutionCenter?v=latest")
170
+ data = parse_response(r, 'data')
171
+ end
172
+
173
+ #####################################################
174
+ # @!group AppVersions
175
+ #####################################################
176
+
177
+ def app_version(app_id, is_live)
178
+ raise "app_id is required" unless app_id
179
+
180
+ v_text = (is_live ? 'live' : nil)
181
+
182
+ r = request(:get, "ra/apps/version/#{app_id}", {v: v_text})
183
+ parse_response(r, 'data')
184
+ end
185
+
186
+ def update_app_version!(app_id, is_live, data)
187
+ raise "app_id is required" unless app_id
188
+
189
+ v_text = (is_live ? 'live' : nil)
190
+
191
+ r = request(:post) do |req|
192
+ req.url "ra/apps/version/save/#{app_id}?v=#{v_text}"
193
+ req.body = data.to_json
194
+ req.headers['Content-Type'] = 'application/json'
195
+ end
196
+
197
+ handle_itc_response(r.body['data'])
198
+ end
199
+
200
+ #####################################################
201
+ # @!group Build Trains
202
+ #####################################################
203
+
204
+ def build_trains(app_id)
205
+ raise "app_id is required" unless app_id
206
+
207
+ r = request(:get, "ra/apps/#{app_id}/trains/")
208
+ data = parse_response(r, 'data')
209
+ end
210
+
211
+ def update_build_trains!(app_id, data)
212
+ raise "app_id is required" unless app_id
213
+
214
+ r = request(:post) do |req|
215
+ req.url "ra/apps/#{app_id}/trains/"
216
+ req.body = data.to_json
217
+ req.headers['Content-Type'] = 'application/json'
218
+ end
219
+
220
+ handle_itc_response(r.body['data'])
221
+ end
222
+
223
+ #####################################################
224
+ # @!group Submit for Review
225
+ #####################################################
226
+
227
+ def send_app_submission(app_id, data, stage)
228
+ raise "app_id is required" unless app_id
229
+
230
+ r = request(:post) do |req|
231
+ req.url "ra/apps/#{app_id}/version/submit/#{stage}"
232
+ req.body = data.to_json
233
+ req.headers['Content-Type'] = 'application/json'
234
+ end
235
+
236
+ handle_itc_response(r.body['data'])
237
+ parse_response(r, 'data')
238
+ end
239
+
240
+ #####################################################
241
+ # @!group Testers
242
+ #####################################################
243
+ def testers(tester)
244
+ url = tester.url[:index]
245
+ r = request(:get, url)
246
+ parse_response(r, 'data')['testers']
247
+ end
248
+
249
+ def testers_by_app(tester, app_id)
250
+ url = tester.url(app_id)[:index_by_app]
251
+ r = request(:get, url)
252
+ parse_response(r, 'data')['users']
253
+ end
254
+
255
+ def create_tester!(tester: nil, email: nil, first_name: nil, last_name: nil)
256
+ url = tester.url[:create]
257
+ raise "Action not provided for this tester type." unless url
258
+
259
+ data = {
260
+ testers: [
261
+ {
262
+ emailAddress: {
263
+ value: email
264
+ },
265
+ firstName: {
266
+ value: first_name
267
+ },
268
+ lastName: {
269
+ value: last_name
270
+ },
271
+ testing: {
272
+ value: true
273
+ }
274
+ }
275
+ ]
276
+ }
277
+
278
+ r = request(:post) do |req|
279
+ req.url url
280
+ req.body = data.to_json
281
+ req.headers['Content-Type'] = 'application/json'
282
+ end
283
+
284
+ data = parse_response(r, 'data')['testers']
285
+ handle_itc_response(data) || data[0]
286
+ end
287
+
288
+ def delete_tester!(tester)
289
+ url = tester.class.url[:delete]
290
+ raise "Action not provided for this tester type." unless url
291
+
292
+ data = [
293
+ {
294
+ emailAddress: {
295
+ value: tester.email
296
+ },
297
+ firstName: {
298
+ value: tester.first_name
299
+ },
300
+ lastName: {
301
+ value: tester.last_name
302
+ },
303
+ testing: {
304
+ value: false
305
+ },
306
+ testerId: tester.tester_id
307
+ }
308
+ ]
309
+
310
+ r = request(:post) do |req|
311
+ req.url url
312
+ req.body = data.to_json
313
+ req.headers['Content-Type'] = 'application/json'
314
+ end
315
+
316
+ data = parse_response(r, 'data')['testers']
317
+ handle_itc_response(data) || data[0]
318
+ end
319
+
320
+ def add_tester_to_app!(tester, app_id)
321
+ update_tester_from_app!(tester, app_id, true)
322
+ end
323
+
324
+ def remove_tester_from_app!(tester, app_id)
325
+ update_tester_from_app!(tester, app_id, false)
326
+ end
327
+
328
+ private
329
+ def update_tester_from_app!(tester, app_id, testing)
330
+ url = tester.class.url(app_id)[:update_by_app]
331
+ data = {
332
+ users: [
333
+ {
334
+ emailAddress: {
335
+ value: tester.email
336
+ },
337
+ firstName: {
338
+ value: tester.first_name
339
+ },
340
+ lastName: {
341
+ value: tester.last_name
342
+ },
343
+ testing: {
344
+ value: testing
345
+ }
346
+ }
347
+ ]
348
+ }
349
+
350
+ r = request(:post) do |req|
351
+ req.url url
352
+ req.body = data.to_json
353
+ req.headers['Content-Type'] = 'application/json'
354
+ end
355
+
356
+ data = parse_response(r, 'data')
357
+ handle_itc_response(data)
358
+ end
359
+ end
360
+ end