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