txgh 1.0.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 (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