txbr 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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