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,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