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,59 @@
1
+ require 'rubygems/package'
2
+ require 'spec_helper'
3
+ require 'stringio'
4
+ require 'zlib'
5
+
6
+ include Txgh::Handlers
7
+
8
+ describe TgzStreamResponse do
9
+ def read_tgz_from(io)
10
+ contents = {}
11
+
12
+ Zlib::GzipReader.wrap(io) do |gz|
13
+ tar = Gem::Package::TarReader.new(gz)
14
+ tar.each do |entry|
15
+ contents[entry.full_name] = entry.read
16
+ end
17
+ end
18
+
19
+ contents
20
+ end
21
+
22
+ let(:attachment) { 'abc123' }
23
+
24
+ let(:enum) do
25
+ {
26
+ 'first_file.yml' => "first\nfile\ncontents\n",
27
+ 'second_file.yml' => "wowowow\nanother file!\n"
28
+ }
29
+ end
30
+
31
+ let(:response) do
32
+ TgzStreamResponse.new(attachment, enum)
33
+ end
34
+
35
+ describe '#write_to' do
36
+ it 'writes a gzipped tar file with the correct entries to the stream' do
37
+ io = StringIO.new('', 'wb')
38
+ response.write_to(io)
39
+ io = io.reopen(io.string, 'rb')
40
+ contents = read_tgz_from(io)
41
+ expect(contents).to eq(enum)
42
+ end
43
+ end
44
+
45
+ describe '#headers' do
46
+ it 'includes the correct content type and disposition headers' do
47
+ expect(response.headers).to eq({
48
+ 'Content-Disposition' => "attachment; filename=\"#{attachment}.tgz\"",
49
+ 'Content-Type' => 'application/x-gtar'
50
+ })
51
+ end
52
+ end
53
+
54
+ describe '#streaming?' do
55
+ it 'returns true' do
56
+ expect(response).to be_streaming
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+ require 'helpers/nil_logger'
3
+ require 'helpers/standard_txgh_setup'
4
+
5
+ include Txgh
6
+ include Txgh::Handlers::Transifex
7
+
8
+ describe HookHandler do
9
+ include StandardTxghSetup
10
+
11
+ let(:requested_resource_slug) do
12
+ resource_slug
13
+ end
14
+
15
+ let(:handler) do
16
+ HookHandler.new(
17
+ project: transifex_project,
18
+ repo: github_repo,
19
+ resource_slug: requested_resource_slug,
20
+ language: language,
21
+ logger: logger
22
+ )
23
+ end
24
+
25
+ let(:downloader) do
26
+ instance_double(ResourceDownloader)
27
+ end
28
+
29
+ before(:each) do
30
+ allow(ResourceDownloader).to receive(:new).and_return(downloader)
31
+ allow(downloader).to(receive(:first)).and_return([
32
+ "translations/#{language}/sample.yml", translations
33
+ ])
34
+ end
35
+
36
+ it 'downloads translations and pushes them to the correct branch (head)' do
37
+ expect(github_api).to(
38
+ receive(:commit).with(
39
+ repo_name, "heads/#{branch}", {
40
+ "translations/#{language}/sample.yml" => translations
41
+ }
42
+ )
43
+ )
44
+
45
+ response = handler.execute
46
+ expect(response.status).to eq(200)
47
+ expect(response.body).to eq(true)
48
+ end
49
+
50
+ it "responds with an error if the config can't be found" do
51
+ expect(handler).to receive(:tx_config).and_return(nil)
52
+ response = handler.execute
53
+ expect(response.status).to eq(404)
54
+ expect(response.body).to eq([
55
+ { error: "Could not find configuration for branch 'heads/#{branch}'" }
56
+ ])
57
+ end
58
+
59
+ context 'with a non-existent resource' do
60
+ let(:requested_resource_slug) { 'foobarbazboo' }
61
+
62
+ it "responds with an error if the resource can't be found" do
63
+ response = handler.execute
64
+ expect(response.status).to eq(404)
65
+ expect(response.body).to eq(
66
+ [{ error: "Could not find resource '#{requested_resource_slug}' in config" }]
67
+ )
68
+ end
69
+ end
70
+
71
+ context 'when asked to process all branches' do
72
+ let(:branch) { 'all' }
73
+ let(:ref) { 'heads/my_branch' }
74
+
75
+ let(:requested_resource_slug) do
76
+ 'my_resource-heads_my_branch'
77
+ end
78
+
79
+ it 'pushes to the individual branch' do
80
+ expect(transifex_api).to receive(:get_resource) do
81
+ { 'categories' => ["branch:#{ref}"] }
82
+ end
83
+
84
+ expect(github_api).to(
85
+ receive(:commit).with(
86
+ repo_name, ref, {
87
+ "translations/#{language}/sample.yml" => translations
88
+ }
89
+ )
90
+ )
91
+
92
+ response = handler.execute
93
+ expect(response.status).to eq(200)
94
+ expect(response.body).to eq(true)
95
+ end
96
+ end
97
+
98
+ context 'with a tag instead of a branch' do
99
+ let(:branch) { 'tags/my_tag' }
100
+
101
+ it 'downloads translations and pushes them to the tag' do
102
+ expect(github_api).to(
103
+ receive(:commit).with(
104
+ repo_name, "tags/my_tag", {
105
+ "translations/#{language}/sample.yml" => translations
106
+ }
107
+ )
108
+ )
109
+
110
+ response = handler.execute
111
+ expect(response.status).to eq(200)
112
+ expect(response.body).to eq(true)
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+ require 'tempfile'
3
+
4
+ include Txgh::Handlers
5
+
6
+ describe ZipStreamResponse do
7
+ def read_zip_from(file)
8
+ contents = {}
9
+
10
+ Zip::File.open(file) do |zipfile|
11
+ zipfile.each do |entry|
12
+ contents[entry.name] = entry.get_input_stream.read
13
+ end
14
+ end
15
+
16
+ contents
17
+ end
18
+
19
+ let(:attachment) { 'abc123' }
20
+
21
+ let(:enum) do
22
+ {
23
+ 'first_file.yml' => "first\nfile\ncontents\n",
24
+ 'second_file.yml' => "wowowow\nanother file!\n"
25
+ }
26
+ end
27
+
28
+ let(:response) do
29
+ ZipStreamResponse.new(attachment, enum)
30
+ end
31
+
32
+ describe '#write_to' do
33
+ it 'writes a zip file with the correct entries to the stream' do
34
+ # this does NOT WORK with a StringIO - zip contents MUST be written to a file
35
+ io = Tempfile.new('testzip')
36
+ response.write_to(io)
37
+ contents = read_zip_from(io.path)
38
+ expect(contents).to eq(enum)
39
+ io.close
40
+ io.unlink
41
+ end
42
+ end
43
+
44
+ describe '#headers' do
45
+ it 'includes the correct content type and disposition headers' do
46
+ expect(response.headers).to eq({
47
+ 'Content-Disposition' => "attachment; filename=\"#{attachment}.zip\"",
48
+ 'Content-Type' => 'application/zip'
49
+ })
50
+ end
51
+ end
52
+
53
+ describe '#streaming?' do
54
+ it 'returns true' do
55
+ expect(response).to be_streaming
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,141 @@
1
+ require 'json'
2
+
3
+ class GithubPayloadBuilder
4
+ class << self
5
+ def push_payload(*args)
6
+ GithubPushPayload.new(*args)
7
+ end
8
+
9
+ def delete_payload(*args)
10
+ GithubDeletePayload.new(*args)
11
+ end
12
+ end
13
+ end
14
+
15
+ class GithubPayload
16
+ def to_h
17
+ # convert symbolized keys to strings
18
+ JSON.parse(to_json)
19
+ end
20
+
21
+ def to_json
22
+ @result.to_json
23
+ end
24
+
25
+ protected
26
+
27
+ def digits
28
+ @@digits ||= ('a'..'f').to_a + ('0'..'9').to_a
29
+ end
30
+
31
+ def generate_timestamp
32
+ Time.now.strftime('%Y-%m-%dT%H:%M:%S%:z')
33
+ end
34
+
35
+ def generate_sha
36
+ blank_commit_id.gsub(/0/) { digits.sample }
37
+ end
38
+
39
+ def blank_commit_id
40
+ '0' * 40
41
+ end
42
+ end
43
+
44
+ class GithubDeletePayload < GithubPayload
45
+ attr_reader :repo, :ref
46
+
47
+ def initialize(repo, ref)
48
+ @repo = repo
49
+ @ref = ref
50
+
51
+ @result = {
52
+ ref: "refs/#{ref}",
53
+ ref_type: 'branch',
54
+ pusher_type: 'user',
55
+
56
+ repository: {
57
+ name: repo.split('/').last,
58
+ full_name: repo,
59
+ owner: {
60
+ login: repo.split('/').first
61
+ }
62
+ },
63
+
64
+ sender: {
65
+ login: repo.split('/').first,
66
+ type: 'User'
67
+ }
68
+ }
69
+ end
70
+ end
71
+
72
+ class GithubPushPayload < GithubPayload
73
+ attr_reader :repo, :ref, :before, :after
74
+
75
+ DEFAULT_USER = {
76
+ name: 'Test User',
77
+ email: 'test@user.com',
78
+ username: 'testuser'
79
+ }
80
+
81
+ def initialize(repo, ref, before = nil, after = nil)
82
+ @repo = repo
83
+ @ref = ref
84
+ @before = before || blank_commit_id
85
+ @after = after || generate_sha
86
+
87
+ @result = {
88
+ ref: "refs/#{ref}",
89
+ before: @before,
90
+ after: @after,
91
+ created: true,
92
+ deleted: false,
93
+ forced: true,
94
+ base_ref: nil,
95
+ compare: "https://github.com/#{@repo}/commit/#{@after[0..12]}",
96
+ commits: [],
97
+ repository: {
98
+ name: repo.split('/').last,
99
+ full_name: repo,
100
+ owner: {
101
+ name: repo.split('/').first
102
+ }
103
+ }
104
+ }
105
+ end
106
+
107
+ def add_commit(options = {})
108
+ id = if commits.empty? && !options.include?(:id)
109
+ after
110
+ else
111
+ options.fetch(:id) { generate_sha }
112
+ end
113
+
114
+ commit_data = {
115
+ id: id,
116
+ distinct: options.fetch(:distinct, true),
117
+ message: options.fetch(:message, 'Default commit message'),
118
+ timestamp: options.fetch(:timestamp) { generate_timestamp },
119
+ url: "https://github.com/#{repo}/commit/#{id}",
120
+ author: options.fetch(:author, DEFAULT_USER),
121
+ committer: options.fetch(:committer, DEFAULT_USER),
122
+ added: options.fetch(:added, []),
123
+ removed: options.fetch(:removed, []),
124
+ modified: options.fetch(:modified, [])
125
+ }
126
+
127
+ if commit_data[:id] == after
128
+ @result[:head_commit] = commit_data
129
+ end
130
+
131
+ commits << commit_data
132
+ end
133
+
134
+ def commits
135
+ @result[:commits]
136
+ end
137
+
138
+ def head_commit
139
+ @result[:head_commit]
140
+ end
141
+ end
@@ -0,0 +1,47 @@
1
+ module IntegrationSetup
2
+ extend RSpec::SharedContext
3
+
4
+ let(:base_config) do
5
+ {
6
+ 'github' => {
7
+ 'repos' => {
8
+ 'txgh-bot/txgh-test-resources' => {
9
+ 'api_username' => 'txgh-bot',
10
+ # github will auto-revoke a token if they notice it in one of your commits ;)
11
+ 'api_token' => Base64.decode64('YjViYWY3Nzk5NTdkMzVlMmI0OGZmYjk4YThlY2M1ZDY0NzAwNWRhZA=='),
12
+ 'push_source_to' => 'test-project-88',
13
+ 'branch' => 'master'
14
+ }
15
+ }
16
+ },
17
+ 'transifex' => {
18
+ 'projects' => {
19
+ 'test-project-88' => {
20
+ 'tx_config' => 'file://./config/tx.config',
21
+ 'api_username' => 'txgh.bot',
22
+ 'api_password' => '2aqFGW99fPRKWvXBPjbrxkdiR',
23
+ 'push_translations_to' => 'txgh-bot/txgh-test-resources'
24
+ }
25
+ }
26
+ }
27
+ }
28
+ end
29
+
30
+ before(:all) do
31
+ VCR.configure do |config|
32
+ config.filter_sensitive_data('<GITHUB_TOKEN>') do
33
+ base_config['github']['repos']['txgh-bot/txgh-test-resources']['api_token']
34
+ end
35
+
36
+ config.filter_sensitive_data('<TRANSIFEX_PASSWORD>') do
37
+ base_config['transifex']['projects']['test-project-88']['api_password']
38
+ end
39
+ end
40
+ end
41
+
42
+ before(:each) do
43
+ allow(Txgh::Config::KeyManager).to(
44
+ receive(:base_config).and_return(base_config)
45
+ )
46
+ end
47
+ end
@@ -0,0 +1,10 @@
1
+ class NilLogger
2
+ def info(*args)
3
+ end
4
+
5
+ def warn(*args)
6
+ end
7
+
8
+ def error(*args)
9
+ end
10
+ end
@@ -0,0 +1,92 @@
1
+ require 'helpers/nil_logger'
2
+ require 'yaml'
3
+
4
+ module StandardTxghSetup
5
+ extend RSpec::SharedContext
6
+
7
+ let(:logger) { NilLogger.new }
8
+ let(:github_api) { double(:github_api) }
9
+ let(:transifex_api) { double(:transifex_api) }
10
+
11
+ let(:project_name) { 'my_awesome_project' }
12
+ let(:resource_slug) { 'my_resource' }
13
+ let(:repo_name) { 'my_org/my_repo' }
14
+ let(:branch) { 'master' }
15
+ let(:tag) { 'all' }
16
+ let(:ref) { 'heads/master' }
17
+ let(:language) { 'ko_KR' }
18
+ let(:translations) { 'translation file contents' }
19
+ let(:diff_point) { nil }
20
+
21
+ let(:project_config) do
22
+ {
23
+ 'api_username' => 'transifex_api_username',
24
+ 'api_password' => 'transifex_api_password',
25
+ 'push_translations_to' => repo_name,
26
+ 'name' => project_name,
27
+ 'tx_config' => "raw://#{tx_config_raw}",
28
+ 'webhook_secret' => 'abc123',
29
+ 'auto_delete_resources' => 'true'
30
+ }
31
+ end
32
+
33
+ let(:repo_config) do
34
+ {
35
+ 'api_username' => 'github_api_username',
36
+ 'api_token' => 'github_api_token',
37
+ 'push_source_to' => project_name,
38
+ 'branch' => branch,
39
+ 'tag' => tag,
40
+ 'name' => repo_name,
41
+ 'webhook_secret' => 'abc123',
42
+ 'diff_point' => diff_point
43
+ }
44
+ end
45
+
46
+ let(:tx_config_raw) do
47
+ """
48
+ [main]
49
+ host = https://www.transifex.com
50
+ lang_map = pt-BR:pt, ko-KR:ko
51
+
52
+ [#{project_name}.#{resource_slug}]
53
+ file_filter = translations/<lang>/sample.yml
54
+ source_file = sample.yml
55
+ source_lang = en
56
+ type = YML
57
+ """
58
+ end
59
+
60
+ let(:tx_config) do
61
+ Txgh::Config::TxConfig.load(tx_config_raw)
62
+ end
63
+
64
+ before(:each) do
65
+ allow(Txgh::Config::KeyManager).to(
66
+ receive(:raw_config) { "raw://#{YAML.dump(base_config)}" }
67
+ )
68
+ end
69
+
70
+ let(:base_config) do
71
+ {
72
+ 'github' => {
73
+ 'repos' => {
74
+ repo_name => repo_config
75
+ }
76
+ },
77
+ 'transifex' => {
78
+ 'projects' => {
79
+ project_name => project_config
80
+ }
81
+ }
82
+ }
83
+ end
84
+
85
+ let(:transifex_project) do
86
+ Txgh::TransifexProject.new(project_config, transifex_api)
87
+ end
88
+
89
+ let(:github_repo) do
90
+ Txgh::GithubRepo.new(repo_config, github_api)
91
+ end
92
+ end