txgh 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +202 -0
  3. data/README.md +64 -0
  4. data/lib/ext/zipline/output_stream.rb +62 -0
  5. data/lib/txgh.rb +53 -0
  6. data/lib/txgh/app.rb +135 -0
  7. data/lib/txgh/category_support.rb +31 -0
  8. data/lib/txgh/config.rb +11 -0
  9. data/lib/txgh/config/config_pair.rb +36 -0
  10. data/lib/txgh/config/key_manager.rb +54 -0
  11. data/lib/txgh/config/provider_instance.rb +20 -0
  12. data/lib/txgh/config/provider_support.rb +26 -0
  13. data/lib/txgh/config/providers.rb +9 -0
  14. data/lib/txgh/config/providers/file_provider.rb +19 -0
  15. data/lib/txgh/config/providers/git_provider.rb +58 -0
  16. data/lib/txgh/config/providers/raw_provider.rb +19 -0
  17. data/lib/txgh/config/tx_config.rb +77 -0
  18. data/lib/txgh/config/tx_manager.rb +15 -0
  19. data/lib/txgh/diff_calculator.rb +90 -0
  20. data/lib/txgh/empty_resource_contents.rb +43 -0
  21. data/lib/txgh/errors.rb +9 -0
  22. data/lib/txgh/github_api.rb +83 -0
  23. data/lib/txgh/github_repo.rb +88 -0
  24. data/lib/txgh/github_request_auth.rb +28 -0
  25. data/lib/txgh/handlers.rb +12 -0
  26. data/lib/txgh/handlers/download_handler.rb +84 -0
  27. data/lib/txgh/handlers/github.rb +10 -0
  28. data/lib/txgh/handlers/github/delete_handler.rb +65 -0
  29. data/lib/txgh/handlers/github/handler.rb +20 -0
  30. data/lib/txgh/handlers/github/push_handler.rb +108 -0
  31. data/lib/txgh/handlers/github/request_handler.rb +106 -0
  32. data/lib/txgh/handlers/response.rb +17 -0
  33. data/lib/txgh/handlers/stream_response.rb +39 -0
  34. data/lib/txgh/handlers/tgz_stream_response.rb +41 -0
  35. data/lib/txgh/handlers/transifex.rb +8 -0
  36. data/lib/txgh/handlers/transifex/hook_handler.rb +77 -0
  37. data/lib/txgh/handlers/transifex/request_handler.rb +78 -0
  38. data/lib/txgh/handlers/triggers.rb +9 -0
  39. data/lib/txgh/handlers/triggers/handler.rb +66 -0
  40. data/lib/txgh/handlers/triggers/pull_handler.rb +29 -0
  41. data/lib/txgh/handlers/triggers/push_handler.rb +21 -0
  42. data/lib/txgh/handlers/zip_stream_response.rb +21 -0
  43. data/lib/txgh/merge_calculator.rb +74 -0
  44. data/lib/txgh/parse_config.rb +24 -0
  45. data/lib/txgh/resource_committer.rb +39 -0
  46. data/lib/txgh/resource_contents.rb +118 -0
  47. data/lib/txgh/resource_downloader.rb +141 -0
  48. data/lib/txgh/resource_updater.rb +104 -0
  49. data/lib/txgh/response_helpers.rb +30 -0
  50. data/lib/txgh/transifex_api.rb +165 -0
  51. data/lib/txgh/transifex_project.rb +37 -0
  52. data/lib/txgh/transifex_request_auth.rb +53 -0
  53. data/lib/txgh/tx_branch_resource.rb +59 -0
  54. data/lib/txgh/tx_logger.rb +12 -0
  55. data/lib/txgh/tx_resource.rb +66 -0
  56. data/lib/txgh/utils.rb +44 -0
  57. data/lib/txgh/version.rb +3 -0
  58. data/spec/app_spec.rb +346 -0
  59. data/spec/category_support_spec.rb +43 -0
  60. data/spec/config/config_pair_spec.rb +47 -0
  61. data/spec/config/key_manager_spec.rb +48 -0
  62. data/spec/config/provider_instance_spec.rb +30 -0
  63. data/spec/config/provider_support_spec.rb +55 -0
  64. data/spec/config/tx_config_spec.rb +49 -0
  65. data/spec/config/tx_manager_spec.rb +57 -0
  66. data/spec/diff_calculator_spec.rb +90 -0
  67. data/spec/github_api_spec.rb +148 -0
  68. data/spec/github_repo_spec.rb +178 -0
  69. data/spec/github_request_auth_spec.rb +39 -0
  70. data/spec/handlers/download_handler_spec.rb +81 -0
  71. data/spec/handlers/github/delete_handler_spec.rb +71 -0
  72. data/spec/handlers/github/push_handler_spec.rb +76 -0
  73. data/spec/handlers/tgz_stream_response_spec.rb +59 -0
  74. data/spec/handlers/transifex/hook_handler_spec.rb +115 -0
  75. data/spec/handlers/zip_stream_response_spec.rb +58 -0
  76. data/spec/helpers/github_payload_builder.rb +141 -0
  77. data/spec/helpers/integration_setup.rb +47 -0
  78. data/spec/helpers/nil_logger.rb +10 -0
  79. data/spec/helpers/standard_txgh_setup.rb +92 -0
  80. data/spec/helpers/test_provider.rb +12 -0
  81. data/spec/integration/cassettes/github_l10n_hook_endpoint.yml +536 -0
  82. data/spec/integration/cassettes/pull.yml +47 -0
  83. data/spec/integration/cassettes/push.yml +544 -0
  84. data/spec/integration/cassettes/transifex_hook_endpoint.yml +560 -0
  85. data/spec/integration/config/tx.config +10 -0
  86. data/spec/integration/hooks_spec.rb +158 -0
  87. data/spec/integration/payloads/github_postbody.json +161 -0
  88. data/spec/integration/payloads/github_postbody_l10n.json +136 -0
  89. data/spec/integration/payloads/github_postbody_release.json +136 -0
  90. data/spec/integration/triggers_spec.rb +45 -0
  91. data/spec/merge_calculator_spec.rb +112 -0
  92. data/spec/parse_config_spec.rb +52 -0
  93. data/spec/resource_committer_spec.rb +42 -0
  94. data/spec/resource_contents_spec.rb +212 -0
  95. data/spec/resource_downloader_spec.rb +205 -0
  96. data/spec/resource_updater_spec.rb +147 -0
  97. data/spec/spec_helper.rb +32 -0
  98. data/spec/transifex_api_spec.rb +345 -0
  99. data/spec/transifex_project_spec.rb +45 -0
  100. data/spec/transifex_request_auth_spec.rb +39 -0
  101. data/spec/tx_branch_resource_spec.rb +99 -0
  102. data/spec/tx_resource_spec.rb +47 -0
  103. data/spec/utils_spec.rb +58 -0
  104. data/txgh.gemspec +29 -0
  105. metadata +296 -0
@@ -0,0 +1,12 @@
1
+ require 'logger'
2
+
3
+ module Txgh
4
+ class TxLogger
5
+ def self.logger
6
+ @_logger ||= Logger.new(STDOUT).tap do |logger|
7
+ logger.level = Logger::INFO
8
+ logger.datetime_format = '%a %d-%m-%Y %H%M '
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,66 @@
1
+ module Txgh
2
+ class TxResource
3
+ attr_reader :project_slug, :resource_slug, :type, :source_lang
4
+ attr_reader :source_file, :translation_file
5
+
6
+ alias_method :original_resource_slug, :resource_slug
7
+
8
+ def initialize(project_slug, resource_slug, type, source_lang, source_file,
9
+ lang_map, translation_file)
10
+ @project_slug = project_slug
11
+ @resource_slug = resource_slug
12
+ @type = type
13
+ @source_lang = source_lang
14
+ @source_file = source_file
15
+ @lang_map = {}
16
+
17
+ if lang_map
18
+ result = {}
19
+ lang_map.split(',').each do |m|
20
+ key_value = m.split(':', 2)
21
+ result[key_value[0].strip] = key_value[1].strip
22
+ end
23
+
24
+ @lang_map = result
25
+ end
26
+
27
+ @translation_file = translation_file
28
+ end
29
+
30
+ def L10N_resource_slug
31
+ "L10N#{resource_slug}"
32
+ end
33
+
34
+ def lang_map(tx_lang)
35
+ @lang_map.fetch(tx_lang, tx_lang)
36
+ end
37
+
38
+ def translation_path(language)
39
+ translation_file.gsub('<lang>', language)
40
+ end
41
+
42
+ def slugs
43
+ [project_slug, resource_slug]
44
+ end
45
+
46
+ def to_h
47
+ {
48
+ project_slug: project_slug,
49
+ resource_slug: resource_slug,
50
+ type: type,
51
+ source_lang: source_lang,
52
+ source_file: source_file,
53
+ translation_file: translation_file
54
+ }
55
+ end
56
+
57
+ def to_api_h
58
+ {
59
+ 'slug' => resource_slug,
60
+ 'i18n_type' => type,
61
+ 'source_language_code' => source_lang,
62
+ 'name' => translation_file
63
+ }
64
+ end
65
+ end
66
+ end
data/lib/txgh/utils.rb ADDED
@@ -0,0 +1,44 @@
1
+ module Txgh
2
+ module Utils
3
+ def slugify(str)
4
+ str.gsub('/', '_')
5
+ end
6
+
7
+ def absolute_branch(branch)
8
+ return unless branch
9
+ if is_tag?(branch)
10
+ branch
11
+ elsif branch.include?('heads/')
12
+ branch
13
+ else
14
+ "heads/#{branch}"
15
+ end
16
+ end
17
+
18
+ def is_tag?(ref)
19
+ ref.include?('tags/')
20
+ end
21
+
22
+ # Builds a hash from an array of hashes using a common key present in all
23
+ # the elements. For example, consider this array of hashes:
24
+ #
25
+ # arr = [
26
+ # { 'param1' => 'dogs', 'param2' => 'hairy' },
27
+ # { 'param1' => 'cats', 'param2' => 'fuzzy' }
28
+ # ]
29
+ #
30
+ # calling index_on('param1', arr) returns:
31
+ #
32
+ # {
33
+ # 'dogs' => { 'param1' => 'dogs', 'param2' => 'hairy' },
34
+ # 'cats' => { 'param1' => 'cats', 'param2' => 'fuzzy' }
35
+ # }
36
+ def index_on(key, arr)
37
+ arr.each_with_object({}) do |hash, ret|
38
+ ret[hash[key]] = hash
39
+ end
40
+ end
41
+ end
42
+
43
+ Utils.extend(Utils)
44
+ end
@@ -0,0 +1,3 @@
1
+ module Txgh
2
+ VERSION = '1.0.0'
3
+ end
data/spec/app_spec.rb ADDED
@@ -0,0 +1,346 @@
1
+ require 'spec_helper'
2
+ require 'rack/test'
3
+ require 'uri'
4
+
5
+ require 'helpers/github_payload_builder'
6
+ require 'helpers/standard_txgh_setup'
7
+
8
+ describe Txgh::Application do
9
+ include Rack::Test::Methods
10
+ include StandardTxghSetup
11
+
12
+ def app
13
+ Txgh::Application
14
+ end
15
+
16
+ describe '/health_check' do
17
+ it 'indicates the server is running, returns a 200' do
18
+ get '/health_check'
19
+ expect(last_response).to be_ok
20
+ expect(JSON.parse(last_response.body)).to eq({})
21
+ end
22
+ end
23
+
24
+ describe '/config' do
25
+ it 'fetches and returns the config for the given project' do
26
+ get '/config', project_slug: project_name
27
+ config = JSON.parse(last_response.body)['data']
28
+ expect(config).to include('resources')
29
+ expect(config).to_not include('branch_slug')
30
+ expect(config['resources'].first).to eq(
31
+ 'project_slug' => 'my_awesome_project',
32
+ 'resource_slug' => 'my_resource',
33
+ 'type' => 'YML',
34
+ 'source_lang' => 'en',
35
+ 'source_file' => 'sample.yml',
36
+ 'translation_file' => 'translations/<lang>/sample.yml'
37
+ )
38
+ end
39
+
40
+ it 'fetches and returns the config for the given project and branch' do
41
+ get '/config', project_slug: project_name, branch: branch
42
+ config = JSON.parse(last_response.body)['data']
43
+ expect(config).to include('resources')
44
+ expect(config['branch_slug']).to eq('heads_master')
45
+ expect(config['resources'].first).to eq(
46
+ 'project_slug' => 'my_awesome_project',
47
+ 'resource_slug' => 'my_resource',
48
+ 'type' => 'YML',
49
+ 'source_lang' => 'en',
50
+ 'source_file' => 'sample.yml',
51
+ 'translation_file' => 'translations/<lang>/sample.yml'
52
+ )
53
+ end
54
+
55
+ it "responds with not found when config can't be found" do
56
+ message = 'Red alert!'
57
+
58
+ expect(Txgh::Config::TxManager).to(
59
+ receive(:tx_config).and_raise(Txgh::ConfigNotFoundError, message)
60
+ )
61
+
62
+ get '/config', project_slug: project_name
63
+ expect(last_response.status).to eq(404)
64
+ response = JSON.parse(last_response.body)
65
+ expect(response).to eq([
66
+ 'error' => message
67
+ ])
68
+ end
69
+
70
+ it 'responds with internal error when an unexpected error occurs' do
71
+ message = 'Red alert!'
72
+
73
+ expect(Txgh::Config::TxManager).to(
74
+ receive(:tx_config).and_raise(StandardError, message)
75
+ )
76
+
77
+ get '/config', project_slug: project_name
78
+ expect(last_response.status).to eq(500)
79
+ response = JSON.parse(last_response.body)
80
+ expect(response).to eq([
81
+ 'error' => message
82
+ ])
83
+ end
84
+ end
85
+ end
86
+
87
+ describe Txgh::Hooks do
88
+ include Rack::Test::Methods
89
+ include StandardTxghSetup
90
+ include Txgh::ResponseHelpers
91
+
92
+ def app
93
+ Txgh::Hooks
94
+ end
95
+
96
+ let(:config) do
97
+ Txgh::Config::ConfigPair.new(project_config, repo_config)
98
+ end
99
+
100
+ before(:each) do
101
+ allow(Txgh::Config::KeyManager).to(
102
+ receive(:config_from_project).with(project_name).and_return(config)
103
+ )
104
+
105
+ allow(Txgh::Config::KeyManager).to(
106
+ receive(:config_from_repo).with(repo_name).and_return(config)
107
+ )
108
+ end
109
+
110
+ describe '/transifex' do
111
+ def sign_with(body)
112
+ header(
113
+ Txgh::TransifexRequestAuth::TRANSIFEX_HEADER,
114
+ Txgh::TransifexRequestAuth.header_value(
115
+ body, config.transifex_project.webhook_secret
116
+ )
117
+ )
118
+ end
119
+
120
+ let(:handler) { double(:handler) }
121
+
122
+ let(:params) do
123
+ {
124
+ 'project' => project_name,
125
+ 'resource' => resource_slug,
126
+ 'language' => language,
127
+ 'translated' => '100'
128
+ }
129
+ end
130
+
131
+ let(:payload) { URI.encode_www_form(params.to_a) }
132
+
133
+ before(:each) do
134
+ allow(Txgh::Handlers::Transifex::HookHandler).to(
135
+ receive(:new) do |options|
136
+ expect(options[:project].name).to eq(project_name)
137
+ expect(options[:repo].name).to eq(repo_name)
138
+ handler
139
+ end
140
+ )
141
+ end
142
+
143
+ it 'creates a handler and executes it' do
144
+ expect(handler).to(
145
+ receive(:execute).and_return(respond_with(200, true))
146
+ )
147
+
148
+ payload = URI.encode_www_form(params.to_a)
149
+ sign_with payload
150
+ post '/transifex', payload
151
+ expect(last_response).to be_ok
152
+ end
153
+
154
+ it 'returns unauthorized if not properly signed' do
155
+ post '/transifex', payload
156
+ expect(last_response.status).to eq(401)
157
+ end
158
+
159
+ it 'returns internal error on unexpected error' do
160
+ expect(Txgh::Config::KeyManager).to(
161
+ receive(:config_from_project).and_raise(StandardError)
162
+ )
163
+
164
+ sign_with payload
165
+ post '/transifex', payload
166
+ expect(last_response.status).to eq(500)
167
+ expect(JSON.parse(last_response.body)).to eq([
168
+ 'error' => 'Internal server error: StandardError'
169
+ ])
170
+ end
171
+ end
172
+
173
+ describe '/github' do
174
+ def sign_with(body)
175
+ header(
176
+ Txgh::GithubRequestAuth::GITHUB_HEADER,
177
+ Txgh::GithubRequestAuth.header_value(
178
+ body, config.github_repo.webhook_secret
179
+ )
180
+ )
181
+ end
182
+
183
+ describe 'push event' do
184
+ let(:handler) { double(:handler) }
185
+
186
+ before(:each) do
187
+ allow(Txgh::Handlers::Github::PushHandler).to(
188
+ receive(:new) do |options|
189
+ expect(options[:project].name).to eq(project_name)
190
+ expect(options[:repo].name).to eq(repo_name)
191
+ handler
192
+ end
193
+ )
194
+ end
195
+
196
+ it 'forwards the request to the github request handler' do
197
+ expect(handler).to(
198
+ receive(:execute).and_return(respond_with(200, true))
199
+ )
200
+
201
+ payload = GithubPayloadBuilder.push_payload(repo_name, ref)
202
+
203
+ sign_with payload.to_json
204
+ header 'X-GitHub-Event', 'push'
205
+ post '/github', payload.to_json
206
+
207
+ expect(last_response).to be_ok
208
+ end
209
+
210
+ it 'returns unauthorized if not properly signed' do
211
+ payload = GithubPayloadBuilder.push_payload(repo_name, ref)
212
+
213
+ header 'X-GitHub-Event', 'push'
214
+ post '/github', payload.to_json
215
+
216
+ expect(last_response.status).to eq(401)
217
+ end
218
+
219
+ it 'returns invalid request if event unrecognized' do
220
+ header 'X-GitHub-Event', 'foobar'
221
+ post '/github'
222
+
223
+ expect(last_response.status).to eq(400)
224
+ end
225
+
226
+ it 'returns internal error on unexpected error' do
227
+ payload = GithubPayloadBuilder.push_payload(repo_name, ref)
228
+
229
+ expect(Txgh::Config::KeyManager).to(
230
+ receive(:config_from_repo).and_raise(StandardError)
231
+ )
232
+
233
+ header 'X-GitHub-Event', 'push'
234
+ post '/github', payload.to_json
235
+
236
+ expect(last_response.status).to eq(500)
237
+ expect(JSON.parse(last_response.body)).to eq([
238
+ 'error' => 'Internal server error: StandardError'
239
+ ])
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ describe Txgh::Triggers do
246
+ include Rack::Test::Methods
247
+ include StandardTxghSetup
248
+
249
+ def app
250
+ Txgh::Triggers
251
+ end
252
+
253
+ let(:config) do
254
+ Txgh::Config::ConfigPair.new(project_config, repo_config)
255
+ end
256
+
257
+ before(:each) do
258
+ allow(Txgh::Config::KeyManager).to(
259
+ receive(:config_from_project).with(project_name).and_return(config)
260
+ )
261
+
262
+ allow(Txgh::Config::KeyManager).to(
263
+ receive(:config_from_repo).with(repo_name).and_return(config)
264
+ )
265
+ end
266
+
267
+ describe '/push' do
268
+ let(:params) do
269
+ { project_slug: project_name, resource_slug: resource_slug, branch: branch }
270
+ end
271
+
272
+ it 'updates the expected resource' do
273
+ updater = double(:updater)
274
+ expect(Txgh::ResourceUpdater).to receive(:new).and_return(updater)
275
+ expect(Txgh::GithubApi).to receive(:new).and_return(github_api)
276
+ expect(github_api).to receive(:get_ref).and_return(object: { sha: 'abc123' })
277
+
278
+ expect(updater).to receive(:update_resource) do |resource, sha|
279
+ expected_branch = Txgh::Utils.absolute_branch(branch)
280
+ expect(resource.branch).to eq(expected_branch)
281
+ expect(resource.project_slug).to eq(project_name)
282
+ expect(resource.resource_slug).to(
283
+ eq("#{resource_slug}-#{Txgh::Utils.slugify(expected_branch)}")
284
+ )
285
+ end
286
+
287
+ patch '/push', params
288
+ expect(last_response).to be_ok
289
+ end
290
+
291
+ it 'returns internal error on unexpected error' do
292
+ expect(Txgh::Config::KeyManager).to(
293
+ receive(:config_from_project).and_raise(StandardError)
294
+ )
295
+
296
+ patch '/push', params
297
+
298
+ expect(last_response.status).to eq(500)
299
+ expect(JSON.parse(last_response.body)).to eq([
300
+ { 'error' => 'Internal server error: StandardError' }
301
+ ])
302
+ end
303
+ end
304
+
305
+ describe '/pull' do
306
+ let(:params) do
307
+ { project_slug: project_name, resource_slug: resource_slug, branch: branch }
308
+ end
309
+
310
+ it 'updates translations (in all locales) in the expected repo' do
311
+ committer = double(:committer)
312
+ languages = [{ 'language_code' => 'pt' }, { 'language_code' => 'ja' }]
313
+ expect(Txgh::ResourceCommitter).to receive(:new).and_return(committer)
314
+ expect(Txgh::TransifexApi).to receive(:new).and_return(transifex_api)
315
+ expect(transifex_api).to receive(:get_languages).and_return(languages)
316
+
317
+ languages.each do |language|
318
+ expect(committer).to receive(:commit_resource) do |resource, branch, lang|
319
+ expect(branch).to eq(branch)
320
+ expect(lang).to eq(language['language_code'])
321
+ expect(resource.branch).to eq(branch)
322
+ expect(resource.project_slug).to eq(project_name)
323
+ expect(resource.resource_slug).to(
324
+ eq("#{resource_slug}-#{Txgh::Utils.slugify(branch)}")
325
+ )
326
+ end
327
+ end
328
+
329
+ patch '/pull', params
330
+ expect(last_response).to be_ok
331
+ end
332
+
333
+ it 'returns internal error on unexpected error' do
334
+ expect(Txgh::Config::KeyManager).to(
335
+ receive(:config_from_project).and_raise(StandardError)
336
+ )
337
+
338
+ patch '/pull', params
339
+
340
+ expect(last_response.status).to eq(500)
341
+ expect(JSON.parse(last_response.body)).to eq([
342
+ { 'error' => 'Internal server error: StandardError' }
343
+ ])
344
+ end
345
+ end
346
+ end