txbr 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/README.md +137 -0
- data/lib/txbr.rb +81 -0
- data/lib/txbr/application.rb +48 -0
- data/lib/txbr/braze_api.rb +42 -0
- data/lib/txbr/braze_session.rb +39 -0
- data/lib/txbr/braze_session_api.rb +73 -0
- data/lib/txbr/commands.rb +14 -0
- data/lib/txbr/config.rb +57 -0
- data/lib/txbr/email_template.rb +70 -0
- data/lib/txbr/email_template_component.rb +94 -0
- data/lib/txbr/email_template_handler.rb +24 -0
- data/lib/txbr/project.rb +50 -0
- data/lib/txbr/request_methods.rb +45 -0
- data/lib/txbr/strings_manifest.rb +55 -0
- data/lib/txbr/tasks.rb +8 -0
- data/lib/txbr/uploader.rb +23 -0
- data/lib/txbr/utils.rb +9 -0
- data/lib/txbr/version.rb +3 -0
- data/spec/application_spec.rb +78 -0
- data/spec/braze_session_api_spec.rb +108 -0
- data/spec/braze_session_spec.rb +49 -0
- data/spec/config_spec.rb +69 -0
- data/spec/email_template_spec.rb +133 -0
- data/spec/fixtures/cassettes/braze_login.yml +324 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/strings_manifest_spec.rb +45 -0
- data/spec/support/env_helpers.rb +13 -0
- data/spec/support/fake_braze_session.rb +14 -0
- data/spec/support/fake_connection.rb +78 -0
- data/spec/support/standard_setup.rb +20 -0
- data/spec/support/test_config.rb +20 -0
- data/spec/uploader_spec.rb +84 -0
- data/spec/utils_spec.rb +17 -0
- data/txbr.gemspec +27 -0
- metadata +190 -0
@@ -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
|
data/lib/txbr/tasks.rb
ADDED
@@ -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
|
data/lib/txbr/utils.rb
ADDED
data/lib/txbr/version.rb
ADDED
@@ -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
|
data/spec/config_spec.rb
ADDED
@@ -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
|