spaceship 0.0.15 → 0.1.0

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