txbr 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.
@@ -0,0 +1,45 @@
1
+ module Txbr
2
+ module RequestMethods
3
+ private
4
+
5
+ def get_json(url, params = {})
6
+ response = get(url, params)
7
+ JSON.parse(response.body)
8
+ end
9
+
10
+ def post_json(url, body = {})
11
+ response = post(url, body.to_json)
12
+ JSON.parse(response.body)
13
+ end
14
+
15
+ def get(url, params = {})
16
+ act(:get, url, params)
17
+ end
18
+
19
+ def post(url, body)
20
+ act(:post, url, body)
21
+ end
22
+
23
+ def act(verb, *args)
24
+ connection.send(verb, *args).tap do |response|
25
+ raise_error!(response)
26
+ end
27
+ end
28
+
29
+ def raise_error!(response)
30
+ case response.status
31
+ when 401
32
+ raise BrazeUnauthorizedError, "401 Unauthorized: #{response.env.url}"
33
+ when 404
34
+ raise BrazeNotFoundError, "404 Not Found: #{response.env.url}"
35
+ else
36
+ if (response.status / 100) != 2
37
+ raise Txbr::BrazeApiError.new(
38
+ "HTTP #{response.status}: #{response.env.url}, body: #{response.body}",
39
+ response.status
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,55 @@
1
+ module Txbr
2
+ class StringsManifest
3
+ include Enumerable
4
+
5
+ def initialize
6
+ @strings ||= {}
7
+ end
8
+
9
+ def add(path, value)
10
+ root = path[0...-1].inject(@strings) do |ret, key|
11
+ ret[key] ||= {}
12
+ end
13
+
14
+ root[path.last] = value
15
+ end
16
+
17
+ def merge(other_manifest)
18
+ self.class.new.tap do |new_manifest|
19
+ new_manifest.merge!(self)
20
+ new_manifest.merge!(other_manifest)
21
+ end
22
+ end
23
+
24
+ def merge!(other_manifest)
25
+ other_manifest.each_string do |path, value|
26
+ add(path, value)
27
+ end
28
+ end
29
+
30
+ def to_h
31
+ @strings
32
+ end
33
+
34
+ def each(&block)
35
+ return to_enum(__method__) unless block_given?
36
+ each_helper(@strings, [], &block)
37
+ end
38
+
39
+ alias each_string each
40
+
41
+ private
42
+
43
+ def each_helper(root, path, &block)
44
+ case root
45
+ when Hash
46
+ root.each_pair do |key, child|
47
+ each_helper(child, path + [key], &block)
48
+ end
49
+
50
+ else
51
+ yield path, root
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,8 @@
1
+ require 'rake'
2
+ require 'txbr'
3
+
4
+ namespace :txbr do
5
+ task :upload_all do
6
+ Txbr::Commands.upload_all
7
+ end
8
+ end
@@ -0,0 +1,23 @@
1
+ module Txbr
2
+ class Uploader
3
+ attr_reader :project
4
+
5
+ def initialize(project)
6
+ @project = project
7
+ end
8
+
9
+ def upload_all
10
+ project.handler.each_resource do |resource|
11
+ upload_resource(resource)
12
+ end
13
+ end
14
+
15
+ def upload_resource(resource)
16
+ stream = StringIO.new
17
+ resource.write_to(stream)
18
+ project.transifex_api.create_or_update(
19
+ resource.tx_resource, stream.string
20
+ )
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ module Txbr
2
+ module Utils
3
+ def url_join(*segments)
4
+ segments.map { |s| s.sub(/\A\/|\/\z/, '') }.join('/')
5
+ end
6
+ end
7
+
8
+ Utils.extend(Utils)
9
+ end
@@ -0,0 +1,3 @@
1
+ module Txbr
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe '/strings.json' do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ Txbr::Application
8
+ end
9
+
10
+ let(:api_client) { double(:api_client) }
11
+
12
+ let(:strings_format) { 'YML' }
13
+ let(:project_slug) { 'myproject' }
14
+ let(:resource_slug) { 'myresource' }
15
+ let(:locale) { 'en' }
16
+
17
+ before do
18
+ allow(Txbr::Config).to(
19
+ receive(:transifex_api_username).and_return('transifex_username')
20
+ )
21
+
22
+ allow(Txbr::Config).to(
23
+ receive(:transifex_api_password).and_return('transifex_password')
24
+ )
25
+
26
+ allow(Txgh::TransifexApi).to(
27
+ receive(:create_from_credentials).and_return(api_client)
28
+ )
29
+ end
30
+
31
+ it 'downloads the resource and returns a JSON version of it' do
32
+ expect(api_client).to(
33
+ receive(:download)
34
+ .with(project_slug, resource_slug, locale)
35
+ .and_return(YAML.dump(locale => { foo: { bar: { baz: 'boo' } } }))
36
+ )
37
+
38
+ params = {
39
+ locale: locale,
40
+ project_slug: project_slug,
41
+ resource_slug: resource_slug,
42
+ strings_format: strings_format
43
+ }
44
+
45
+ get '/strings.json', params
46
+
47
+ expect(last_response).to be_ok
48
+ expect(last_response.body).to eq(
49
+ { foo: { bar: { baz: 'boo' } } }.to_json
50
+ )
51
+ end
52
+
53
+ it 'sends back an error response if a param is missing' do
54
+ get '/strings.json'
55
+ expect(last_response.status).to eq(400)
56
+ expect(JSON.parse(last_response.body)).to(
57
+ eq('error' => "Missing parameter 'project_slug'")
58
+ )
59
+ end
60
+
61
+ it 'sends back an error response if an unexpected error occurs' do
62
+ expect(api_client).to receive(:download).and_raise('jelly beans')
63
+
64
+ params = {
65
+ locale: locale,
66
+ project_slug: project_slug,
67
+ resource_slug: resource_slug,
68
+ strings_format: strings_format
69
+ }
70
+
71
+ get '/strings.json', params
72
+
73
+ expect(last_response.status).to eq(500)
74
+ expect(JSON.parse(last_response.body)).to(
75
+ eq('error' => 'jelly beans')
76
+ )
77
+ end
78
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+ require 'support/fake_braze_session'
3
+ require 'support/fake_connection'
4
+
5
+ describe Txbr::BrazeSessionApi do
6
+ let(:session_id) { 'session_id' }
7
+ let(:app_group_id) { 'app_group_id' }
8
+ let(:api_url) { 'https://somewhere.braze.com' }
9
+ let(:session) { FakeBrazeSession.new(api_url, session_id) }
10
+ let(:connection) { FakeConnection.new(interactions) }
11
+ let(:client) { described_class.new(session, app_group_id, connection: connection) }
12
+
13
+ shared_examples 'a client request that handles errors' do
14
+ context 'when the resource is not found' do
15
+ let(:interactions) do
16
+ super().tap do |inter|
17
+ inter[0][:response][:status] = 404
18
+ end
19
+ end
20
+
21
+ it 'raises an error' do
22
+ expect { subject }.to raise_error(Txbr::BrazeNotFoundError)
23
+ end
24
+ end
25
+
26
+ context 'when the request is unauthorized' do
27
+ let(:interactions) do
28
+ super().tap do |inter|
29
+ inter.unshift(
30
+ request: inter[0][:request],
31
+ response: { status: 401 }
32
+ )
33
+ end
34
+ end
35
+
36
+ it 'resets the session and tries again' do
37
+ expect { subject }.to_not raise_error
38
+ expect(session).to be_reset
39
+ end
40
+ end
41
+
42
+ context 'when some other bad thing happens' do
43
+ let(:interactions) do
44
+ super().tap do |inter|
45
+ inter[0][:response][:status] = 500
46
+ end
47
+ end
48
+
49
+ it 'raises an error' do
50
+ expect { subject }.to raise_error(Txbr::BrazeApiError)
51
+ end
52
+ end
53
+ end
54
+
55
+ describe '#each_email_template' do
56
+ before do
57
+ stub_const("#{described_class.name}::EMAIL_TEMPLATE_BATCH_SIZE", 1)
58
+ end
59
+
60
+ subject { client.each_email_template.to_a }
61
+
62
+ let(:interactions) do
63
+ [{
64
+ request: { verb: 'get', url: 'engagement/email_templates', start: 0, length: 1 },
65
+ response: { status: 200, body: { results: [{ id: '123abc' }] }.to_json }
66
+ }, {
67
+ request: { verb: 'get', url: 'engagement/email_templates', start: 1, length: 1 },
68
+ response: { status: 200, body: { results: [{ id: '456def' }] }.to_json }
69
+ }, {
70
+ request: { verb: 'get', url: 'engagement/email_templates', start: 2, length: 0 },
71
+ response: { status: 200, body: { results: [] }.to_json }
72
+ }]
73
+ end
74
+
75
+ it 'yields each template' do
76
+ expect(subject).to eq([{ 'id' => '123abc' }, { 'id' => '456def' }])
77
+ end
78
+
79
+ it_behaves_like 'a client request that handles errors'
80
+ end
81
+
82
+ describe '#get_email_template_details' do
83
+ subject { client.get_email_template_details(email_template_id: email_template_id) }
84
+ let(:email_template_id) { 'abc123' }
85
+
86
+ let(:details) do
87
+ {
88
+ 'name' => 'Foo Template',
89
+ 'template' => "<html><body>I'm a little teapot</body</html>",
90
+ 'subject' => 'Subject subject',
91
+ 'preheader' => 'Preheader preheader',
92
+ }
93
+ end
94
+
95
+ let(:interactions) do
96
+ [{
97
+ request: { verb: 'get', url: "engagement/email_templates/#{email_template_id}" },
98
+ response: { status: 200, body: details.to_json }
99
+ }]
100
+ end
101
+
102
+ it 'retrieves the template details' do
103
+ expect(subject).to eq(details)
104
+ end
105
+
106
+ it_behaves_like 'a client request that handles errors'
107
+ end
108
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe Txbr::BrazeSession do
4
+ # we need to hard-code this in order for VCR to successfully match requests
5
+ # after the initial recording
6
+ let(:api_url) { 'https://dashboard-03.braze.com' }
7
+
8
+ # NOTE: you will need to provide these env variables if you want to
9
+ # re-generate the VCR cassettes that depend on Braze login.
10
+ let(:email_address) { ENV['BRAZE_EMAIL_ADDRESS'] || 'BRAZE_EMAIL_ADDRESS' }
11
+ let(:password) { ENV['BRAZE_PASSWORD'] || 'BRAZE_PASSWORD' }
12
+
13
+ let(:session) { described_class.new(api_url, email_address, password) }
14
+
15
+ describe '#session_id' do
16
+ around do |example|
17
+ VCR.use_cassette('braze_login') { example.run }
18
+ end
19
+
20
+ it 'logs in and is issued a session id' do
21
+ expect(session.session_id).to match(/[a-z0-9]{32}/)
22
+ end
23
+
24
+ it 'does not create a new session if called more than once' do
25
+ # expect mechanize to be invoked exactly once
26
+ expect(Mechanize).to receive(:new).and_call_original.once
27
+
28
+ # grab the session ID multiple times, which should invoke
29
+ # mechanize the first time only
30
+ session.session_id
31
+ session.session_id
32
+ end
33
+ end
34
+
35
+ describe '#reset!' do
36
+ it 'causes a new session to be requested' do
37
+ # mechanize should be invoked twice, once before the session is reset
38
+ # and once after
39
+ expect(Mechanize).to receive(:new).and_call_original.twice
40
+
41
+ # we have to wrap the session_id calls in separate use_cassette calls
42
+ # because VCR isn't smart enough to stub the requests more than once
43
+ # (it would be great if cassettes had a "rewind" option, but alas)
44
+ VCR.use_cassette('braze_login') { session.session_id }
45
+ session.reset!
46
+ VCR.use_cassette('braze_login') { session.session_id }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+ require 'tempfile'
3
+ require 'yaml'
4
+
5
+ require 'support/test_config'
6
+
7
+ describe Txbr::Config do
8
+ before(:each) do
9
+ # clear out config before each test
10
+ Txbr::Config.instance_variable_set(:@raw_config, nil)
11
+ end
12
+
13
+ let(:config) { TestConfig.config }
14
+
15
+ shared_examples 'a successful configuration' do
16
+ it 'is configured correctly' do
17
+ expect(described_class.transifex_api_username).to eq('transifex_username')
18
+ expect(described_class.transifex_api_password).to eq('transifex_password')
19
+
20
+ expect(Txbr::Config.projects.size).to eq(1)
21
+ project = Txbr::Config.projects.first
22
+
23
+ expect(project.handler_id).to eq('email-templates')
24
+ expect(project.braze_api_key).to eq('braze_api_key')
25
+ expect(project.braze_api_url).to eq('https://somewhere.braze.com')
26
+ expect(project.strings_format).to eq('YML')
27
+ expect(project.source_lang).to eq('en')
28
+
29
+ # @TODO: remove once Braze implements the endpoints we asked for
30
+ expect(project.braze_email_address).to eq('braze@email.com')
31
+ expect(project.braze_password).to eq('braze_password')
32
+ expect(project.braze_app_group_id).to eq('5551212')
33
+ end
34
+ end
35
+
36
+ describe '.projects' do
37
+ context 'when config is specified as a string' do
38
+ around do |example|
39
+ with_env('TXBR_CONFIG' => "raw://#{YAML.dump(config)}") do
40
+ example.run
41
+ end
42
+ end
43
+
44
+ it_behaves_like 'a successful configuration'
45
+ end
46
+
47
+ context 'when config is specified as a file' do
48
+ let(:file) do
49
+ Tempfile.new('config').tap do |file|
50
+ file.write(YAML.dump(config))
51
+ file.flush
52
+ end
53
+ end
54
+
55
+ around do |example|
56
+ with_env('TXBR_CONFIG' => "file://#{file.path}") do
57
+ example.run
58
+ end
59
+ end
60
+
61
+ after do
62
+ file.close
63
+ file.unlink
64
+ end
65
+
66
+ it_behaves_like 'a successful configuration'
67
+ end
68
+ end
69
+ end