adcloud 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +69 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +50 -0
- data/LICENSE.txt +20 -0
- data/README.md +221 -0
- data/RELEASE_NOTES.md +14 -0
- data/Rakefile +8 -0
- data/adcloud.gemspec +27 -0
- data/lib/adcloud.rb +62 -0
- data/lib/adcloud/advertisement.rb +28 -0
- data/lib/adcloud/api_error.rb +33 -0
- data/lib/adcloud/authentication.rb +24 -0
- data/lib/adcloud/campaign.rb +69 -0
- data/lib/adcloud/connection.rb +46 -0
- data/lib/adcloud/customer.rb +9 -0
- data/lib/adcloud/entity.rb +84 -0
- data/lib/adcloud/exception_raiser.rb +22 -0
- data/lib/adcloud/media_file.rb +10 -0
- data/lib/adcloud/product.rb +10 -0
- data/lib/adcloud/report.rb +28 -0
- data/lib/adcloud/report_entry.rb +48 -0
- data/lib/adcloud/response_error_handler.rb +23 -0
- data/lib/adcloud/topic.rb +19 -0
- data/lib/adcloud/topic_discount.rb +16 -0
- data/lib/adcloud/version.rb +3 -0
- data/lib/adcloud/webhook.rb +35 -0
- data/lib/adcloud/webhook_config.rb +18 -0
- data/lib/adcloud/webhook_event.rb +18 -0
- data/test/adcloud/advertisement_test.rb +11 -0
- data/test/adcloud/authentication_test.rb +58 -0
- data/test/adcloud/campaign_test.rb +54 -0
- data/test/adcloud/connection_test.rb +78 -0
- data/test/adcloud/customer_test.rb +6 -0
- data/test/adcloud/entity_test.rb +159 -0
- data/test/adcloud/media_file_test.rb +7 -0
- data/test/adcloud/product_test.rb +5 -0
- data/test/adcloud/report_test.rb +34 -0
- data/test/adcloud/topic_test.rb +14 -0
- data/test/adcloud/webhook_event_test.rb +19 -0
- data/test/adcloud/webhook_test.rb +62 -0
- data/test/adcloud_test.rb +47 -0
- data/test/test_helper.rb +23 -0
- metadata +251 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module Adcloud
|
2
|
+
class Topic < Entity
|
3
|
+
attribute :id, Integer
|
4
|
+
attribute :start_prio, Integer
|
5
|
+
attribute :modified, DateTime
|
6
|
+
attribute :created, DateTime
|
7
|
+
attribute :discounts, Hash
|
8
|
+
attribute :names, Hash
|
9
|
+
|
10
|
+
def discounts=(data)
|
11
|
+
@discounts = data.reduce({}) do |hash, raw_discount|
|
12
|
+
discount = TopicDiscount.new(raw_discount)
|
13
|
+
hash[discount.country_code] = discount
|
14
|
+
hash
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Adcloud
|
2
|
+
class TopicDiscount
|
3
|
+
include Virtus
|
4
|
+
|
5
|
+
attribute :country_id, Integer
|
6
|
+
attribute :country_code, String
|
7
|
+
attribute :discount, Float
|
8
|
+
attribute :reach, Float
|
9
|
+
attribute :min_price_cpc, Float
|
10
|
+
attribute :price_cpc, Float
|
11
|
+
attribute :max_price_cpc, Float
|
12
|
+
attribute :min_price_cpm, Float
|
13
|
+
attribute :price_cpm, Float
|
14
|
+
attribute :max_price_cpm, Float
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Adcloud
|
2
|
+
class Webhook
|
3
|
+
ALL = [:on_topic_price_update, :on_campaign_update]
|
4
|
+
|
5
|
+
attr_accessor :events
|
6
|
+
|
7
|
+
# @param events [Array<Hash>, String] Events
|
8
|
+
def initialize(events)
|
9
|
+
self.events = events.kind_of?(Array) ? events : JSON.parse(events.to_s)
|
10
|
+
rescue
|
11
|
+
raise ArgumentError.new("Invalid webhook event data!")
|
12
|
+
end
|
13
|
+
|
14
|
+
def events=(events)
|
15
|
+
@events = [events].flatten.map { |event| Adcloud::WebhookEvent.new(event) }
|
16
|
+
if Adcloud.config.webhooks.filter_tests
|
17
|
+
@events.reject! { |event| event.test_data? }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def process!
|
22
|
+
self.events.each do |event|
|
23
|
+
proc = case event.type
|
24
|
+
when 'TopicPrice.update'
|
25
|
+
:on_topic_price_update
|
26
|
+
when 'Booking.update'
|
27
|
+
:on_campaign_update
|
28
|
+
else
|
29
|
+
:on_unknown_webhook
|
30
|
+
end
|
31
|
+
Adcloud.config.webhooks.send(proc).call(event)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Adcloud
|
2
|
+
class WebhookConfig < ActiveSupport::Configurable::Configuration
|
3
|
+
def initialize
|
4
|
+
Adcloud::Webhook::ALL.each do |hook|
|
5
|
+
self.send("#{hook}=", WebhookConfig.method(:unused_webhook))
|
6
|
+
end
|
7
|
+
self.on_unknown_webhook = WebhookConfig.method(:unknown_webhook)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.unused_webhook(event)
|
11
|
+
Adcloud.logger.warn { "Webhook behaviour missing for #{event.inspect}" }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.unknown_webhook(event)
|
15
|
+
Adcloud.logger.warn { "Unknown webhook event #{event.inspect}. Please contact gem maintainer!" }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Adcloud
|
2
|
+
class WebhookEvent
|
3
|
+
attr_reader :data, :meta
|
4
|
+
|
5
|
+
def initialize(raw_response)
|
6
|
+
@meta = raw_response.delete('_meta') || {}
|
7
|
+
@data = raw_response || {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def type
|
11
|
+
@meta['event']
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_data?
|
15
|
+
@meta.has_key?('is_test_data') && @meta['is_test_data']
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
describe Adcloud::Advertisement do
|
5
|
+
|
6
|
+
it 'should set the correct api endpoint' do
|
7
|
+
Adcloud::Advertisement.api_endpoint.must_equal 'advertisements'
|
8
|
+
end
|
9
|
+
|
10
|
+
# for more tests checkout entity tests
|
11
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Adcloud::Authentication do
|
4
|
+
|
5
|
+
describe "initialization" do
|
6
|
+
|
7
|
+
describe "with a valid hash" do
|
8
|
+
before do
|
9
|
+
@adcloud_auth = Adcloud::Authentication.new(:client_id => "0987654321", :client_secret => "1234567890")
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should have a client_id 0987654321" do
|
13
|
+
@adcloud_auth.client_id.must_equal "0987654321"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should have a client_secret 1234567890" do
|
17
|
+
@adcloud_auth.client_secret.must_equal "1234567890"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "with an empty hash" do
|
23
|
+
|
24
|
+
before do
|
25
|
+
@adcloud_auth = Adcloud::Authentication.new({})
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should have a client_id and client_secret being nil" do
|
29
|
+
@adcloud_auth.client_secret.must_be_nil
|
30
|
+
@adcloud_auth.client_id.must_be_nil
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "is successful" do
|
38
|
+
|
39
|
+
it "should return a token" do
|
40
|
+
stub_request(:post, "https://api.adcloud.com:443/v2/oauth/access_token").with(:body => {"client_id"=>"0987654321", "client_secret"=>"1234567890", "grant_type"=>"none"}, :headers => {'Accept'=>'*/*', 'Content-Type'=>'application/x-www-form-urlencoded', 'User-Agent'=>'Ruby'}).to_return(:status => 200, :body => {"_meta" => {"access_token" => "bab0e5c477f211c4612345678907498b6e55600"}, "access_token" => "bab0e5c477f211c4612345678907498b6e55600","scope" => ""}, :headers => {})
|
41
|
+
adcloud_auth = Adcloud::Authentication.new(:client_id => "0987654321", :client_secret => "1234567890")
|
42
|
+
adcloud_auth.authenticate!
|
43
|
+
adcloud_auth.token.must_equal "bab0e5c477f211c4612345678907498b6e55600"
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
# describe "is unsuccessful" do
|
49
|
+
|
50
|
+
# it "should raise an authentication error" do
|
51
|
+
# stub_request(:post, "https://api.adcloud.com:443/v2/oauth/access_token").with(:body => {"client_id"=>"0987654321", "client_secret"=>"1234567890", "grant_type"=>"none"}, :headers => {'Accept'=>'*/*', 'Content-Type'=>'application/x-www-form-urlencoded', 'User-Agent'=>'Ruby'}).to_return(:status => 401, :body => {"status" => 401, "errors" => [{"message" => "Bad credentials"}]}, :headers => {})
|
52
|
+
# adcloud_auth = Adcloud::Authentication.new(:client_id => "0987654321", :client_secret => "1234567890")
|
53
|
+
# -> { adcloud_auth.authenticate! }.must_raise(Adcloud::AuthenticationError)
|
54
|
+
# end
|
55
|
+
|
56
|
+
# end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
describe Adcloud::Campaign do
|
5
|
+
subject { Adcloud::Campaign }
|
6
|
+
|
7
|
+
let(:campaign) { subject.new }
|
8
|
+
let(:connection) { stub() }
|
9
|
+
|
10
|
+
before do
|
11
|
+
subject.stubs(:connection => connection)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "validate" do
|
15
|
+
let(:request_attributes) { campaign.attributes.select { |k, v| ![:_meta, :id].include?(k) } }
|
16
|
+
|
17
|
+
it 'should validate against the api' do
|
18
|
+
connection.expects(:get).with('campaigns/validate', { campaign: request_attributes }).returns({'_meta' => { 'status' => 226, 'details' => {}}})
|
19
|
+
campaign.validate
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should populate the errors hash' do
|
23
|
+
response_data = {'_meta' => { 'status' => 226, 'details' => { 'company_id' => ['must be present'] } } }
|
24
|
+
connection.stubs(:get).returns(response_data)
|
25
|
+
campaign.validate
|
26
|
+
campaign.errors['company_id'].must_equal ['must be present']
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'raises an exception when the response is not well formatted' do
|
30
|
+
connection.expects(:get).with('campaigns/validate', { campaign: request_attributes })
|
31
|
+
-> { campaign.validate }.must_raise(Adcloud::AdcloudSucks::InvalidApiResponse)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#valid?' do
|
36
|
+
it 'validates the model' do
|
37
|
+
campaign.expects(:validate)
|
38
|
+
campaign.valid?
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'returns true when object is valid' do
|
42
|
+
campaign.stubs(:validate)
|
43
|
+
campaign.errors = {}
|
44
|
+
campaign.valid?.must_equal true
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'returns false when the object is invalid' do
|
48
|
+
campaign.stubs(:validate)
|
49
|
+
campaign.errors = { company_id: ['must be present'] }
|
50
|
+
campaign.valid?.must_equal false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Adcloud::Connection do
|
4
|
+
|
5
|
+
subject { Adcloud::Connection.new }
|
6
|
+
|
7
|
+
let(:authentication) { stub(:authenticate! => true, :token => "0987654321") }
|
8
|
+
|
9
|
+
describe "url" do
|
10
|
+
|
11
|
+
it "should be https://api.adcloud.com:443/" do
|
12
|
+
subject.url.must_equal "https://api.adcloud.com:443/v2/"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "authentication token" do
|
18
|
+
|
19
|
+
it "should be 0987654321" do
|
20
|
+
subject.expects(:authentication).returns(authentication)
|
21
|
+
subject.authentication_token.must_equal "0987654321"
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "authentication" do
|
27
|
+
it "should return an authentication object" do
|
28
|
+
Adcloud::Authentication.expects(:new).returns(authentication)
|
29
|
+
subject.authentication.must_equal authentication
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "an authenticated connection" do
|
34
|
+
let(:url) { "https://api.adcloud.com:443/v2/whatever" }
|
35
|
+
|
36
|
+
before do
|
37
|
+
subject.stubs(:authentication_token).returns("0987654321")
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "post" do
|
41
|
+
|
42
|
+
it "should fire a post request" do
|
43
|
+
stub_request(:post, url).to_return(:status => 200, :body => {"_meta" => {}, "hello" => "world"}, :headers => {})
|
44
|
+
subject.post('whatever').must_equal({"_meta" => {}, "hello" => "world" })
|
45
|
+
end
|
46
|
+
|
47
|
+
# it "should raise an InvalidRequest Exception" do
|
48
|
+
# stub_request(:post, url).to_return(:status => 500, :body => "{}", :headers => {})
|
49
|
+
# -> { subject.post('whatever') }.must_raise(Adcloud::InvalidRequest)
|
50
|
+
# end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "get" do
|
55
|
+
|
56
|
+
it "should fire a get request" do
|
57
|
+
stub_request(:get, url).to_return(:status => 200, :body => {"_meta" => {}, "hello" => "world"}, :headers => {})
|
58
|
+
subject.get('whatever').must_equal({"_meta" => {}, "hello" => "world" })
|
59
|
+
end
|
60
|
+
|
61
|
+
# it "should raise an InvalidFilter Exception" do
|
62
|
+
# stub_request(:get, url).to_return(:status => 400, :body => "{}", :headers => {})
|
63
|
+
# -> { subject.get('whatever') }.must_raise(Adcloud::InvalidFilter)
|
64
|
+
# end
|
65
|
+
|
66
|
+
# it "should raise a NotFound Exception" do
|
67
|
+
# stub_request(:get, url).to_return(:status => 404, :body => "{}", :headers => {})
|
68
|
+
# -> { subject.get('whatever') }.must_raise(Adcloud::NotFound)
|
69
|
+
# end
|
70
|
+
|
71
|
+
# it "should raise an InvalidRequest Exception" do
|
72
|
+
# stub_request(:get, url).to_return(:status => 500, :body => "{}", :headers => {})
|
73
|
+
# -> { subject.get('whatever') }.must_raise(Adcloud::InvalidRequest)
|
74
|
+
# end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
describe Adcloud::Entity do
|
5
|
+
|
6
|
+
class Car < Adcloud::Entity
|
7
|
+
#self.api_endpoint = 'cars'
|
8
|
+
attribute :id, Integer
|
9
|
+
attribute :name, String
|
10
|
+
end
|
11
|
+
|
12
|
+
subject { Car }
|
13
|
+
|
14
|
+
let(:connection) { stub }
|
15
|
+
|
16
|
+
describe ".connection" do
|
17
|
+
it "should return a connection object" do
|
18
|
+
Adcloud::Connection.expects(:new).returns(connection)
|
19
|
+
subject.connection.must_equal(connection)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
describe "#connection" do
|
25
|
+
it "should return the class level connection object" do
|
26
|
+
subject.expects(:connection).returns(connection)
|
27
|
+
subject.new.connection.must_equal(connection)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '.api_endpoint' do
|
32
|
+
it 'its api endpoint is cars' do
|
33
|
+
subject.api_endpoint.must_equal 'cars'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "find" do
|
38
|
+
let(:response_data) { { 'id' => 42, 'name' => 'Porsche' } }
|
39
|
+
|
40
|
+
it "should return a car" do
|
41
|
+
subject.connection.expects(:get).with('cars/42').returns(response_data)
|
42
|
+
subject.find(42).must_be_instance_of(Car)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "all" do
|
47
|
+
let(:empty_response_data) { {"_meta"=>{"type"=>"Array<>", "page"=>1, "size"=>0, "per_page"=>50, "total_count"=>0, "total_pages"=>0, "sort"=>{}, "uuid"=>"aabe4f5f31691f049ea1942e6a6d1793"}, "items"=>[]} }
|
48
|
+
let(:response_data) { { "_meta" => { "type" => "Array<Car>", "page" => 1, "size" => 2, "per_page" => 50, "total_count" => 2, "total_pages" => 1, "sort" => {}, "uuid" => "aabe4f5f31691f049ea1942e6a6d1793" }, "items" => [{ "id" => 52654, "name" => 'Mercedes', "_meta" => { "type" => "Car" } }, { "id" => 52655, "name" => 'Audi', "_meta" => { "type" => "Car" } }]} }
|
49
|
+
|
50
|
+
it "should return an empty array if response is empty" do
|
51
|
+
subject.connection.expects(:get).with('cars', :filter => {}, :page => 1, :per_page => 50).returns(empty_response_data)
|
52
|
+
subject.all.must_be_instance_of(Array)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should return an array if response contains objects' do
|
56
|
+
subject.connection.expects(:get).with('cars', :filter => {}, :page => 1, :per_page => 50).returns(response_data)
|
57
|
+
subject.all.must_be_instance_of(Array)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should return an array of cars' do
|
61
|
+
subject.connection.expects(:get).with('cars', :filter => {}, :page => 1, :per_page => 50).returns(response_data)
|
62
|
+
subject.all.each { |item| item.must_be_instance_of(subject) }
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should return 2 cars' do
|
66
|
+
subject.connection.expects(:get).with('cars', :filter => {}, :page => 1, :per_page => 50).returns(response_data)
|
67
|
+
subject.all.size.must_equal 2
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should pass the filter param' do
|
71
|
+
subject.connection.expects(:get).with('cars', :filter => { :oliver => "ist chef" }, :page => 1, :per_page => 50).returns(response_data)
|
72
|
+
subject.all(:oliver => "ist chef")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#create' do
|
77
|
+
before { subject.stubs(:connection => connection) }
|
78
|
+
|
79
|
+
describe 'when submitting a valid object' do
|
80
|
+
let(:response) { { '_meta' => { 'status' => 200 }, 'id' => 1 } }
|
81
|
+
|
82
|
+
it 'sends a request to the api' do
|
83
|
+
car = subject.new
|
84
|
+
attributes = car.attributes
|
85
|
+
attributes.delete(:id)
|
86
|
+
attributes.delete(:_meta)
|
87
|
+
connection.expects(:post).with('cars', { 'car' => attributes }).returns(response)
|
88
|
+
car.create
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'sets the car id' do
|
92
|
+
connection.stubs(:post).returns(response)
|
93
|
+
car = subject.new
|
94
|
+
car.create
|
95
|
+
car.id.must_equal 1
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'returns true' do
|
99
|
+
connection.stubs(:post).returns(response)
|
100
|
+
subject.new.create.must_equal true
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'when submitting an invalid car' do
|
105
|
+
let(:error) { Adcloud::BadRequestError.new(stub(:body => {'_meta' => { 'details' => { :name => ['cannot be empty'] }}})) }
|
106
|
+
before do
|
107
|
+
subject.stubs(:connection => connection)
|
108
|
+
connection.stubs(:post).raises(error)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'returns false' do
|
112
|
+
subject.new.create.must_equal false
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'sets the errors hash' do
|
116
|
+
car = subject.new
|
117
|
+
car.create
|
118
|
+
car.errors.must_equal({ :name => ['cannot be empty'] })
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe '.create' do
|
124
|
+
it 'creates a new car' do
|
125
|
+
car = subject.new
|
126
|
+
subject.expects(:new).returns(car)
|
127
|
+
car.expects(:create)
|
128
|
+
subject.create.must_equal car
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "errors" do
|
133
|
+
it "should be empty" do
|
134
|
+
subject.new.errors.must_be_empty
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe '.api_name' do
|
139
|
+
it 'returns the class name in lowercase' do
|
140
|
+
Car.api_name.must_equal 'car'
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'removes the namespace' do
|
144
|
+
module MyTest
|
145
|
+
class Truck < Adcloud::Entity
|
146
|
+
end
|
147
|
+
end
|
148
|
+
MyTest::Truck.api_name.must_equal 'truck'
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'transforms camel case to underscore' do
|
152
|
+
class AirPlane < Adcloud::Entity
|
153
|
+
end
|
154
|
+
AirPlane.api_name.must_equal 'air_plane'
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|