pisoni 1.23.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/Gemfile +16 -0
  4. data/LICENSE +202 -0
  5. data/Makefile +73 -0
  6. data/NOTICE +15 -0
  7. data/README.md +47 -0
  8. data/Rakefile +27 -0
  9. data/lib/3scale/core/alert_limit.rb +32 -0
  10. data/lib/3scale/core/api_client/attributes.rb +56 -0
  11. data/lib/3scale/core/api_client/collection.rb +19 -0
  12. data/lib/3scale/core/api_client/operations.rb +233 -0
  13. data/lib/3scale/core/api_client/resource.rb +52 -0
  14. data/lib/3scale/core/api_client/support.rb +55 -0
  15. data/lib/3scale/core/api_client.rb +5 -0
  16. data/lib/3scale/core/application.rb +67 -0
  17. data/lib/3scale/core/application_key.rb +34 -0
  18. data/lib/3scale/core/application_referrer_filter.rb +33 -0
  19. data/lib/3scale/core/errors.rb +130 -0
  20. data/lib/3scale/core/event.rb +26 -0
  21. data/lib/3scale/core/logger.rb +12 -0
  22. data/lib/3scale/core/metric.rb +32 -0
  23. data/lib/3scale/core/service.rb +82 -0
  24. data/lib/3scale/core/service_error.rb +34 -0
  25. data/lib/3scale/core/service_token.rb +39 -0
  26. data/lib/3scale/core/transaction.rb +26 -0
  27. data/lib/3scale/core/usage_limit.rb +40 -0
  28. data/lib/3scale/core/user.rb +47 -0
  29. data/lib/3scale/core/utilization.rb +28 -0
  30. data/lib/3scale/core/version.rb +5 -0
  31. data/lib/3scale/core.rb +63 -0
  32. data/lib/3scale_core.rb +1 -0
  33. data/lib/pisoni.rb +5 -0
  34. data/pisoni.gemspec +43 -0
  35. data/spec/alert_limit_spec.rb +72 -0
  36. data/spec/application_key_spec.rb +97 -0
  37. data/spec/application_referrer_filter_spec.rb +57 -0
  38. data/spec/application_spec.rb +188 -0
  39. data/spec/event_spec.rb +82 -0
  40. data/spec/metric_spec.rb +115 -0
  41. data/spec/private_endpoints/event.rb +9 -0
  42. data/spec/private_endpoints/service_error.rb +10 -0
  43. data/spec/private_endpoints/transaction.rb +16 -0
  44. data/spec/service_error_spec.rb +128 -0
  45. data/spec/service_spec.rb +159 -0
  46. data/spec/service_token_spec.rb +121 -0
  47. data/spec/spec_helper.rb +15 -0
  48. data/spec/transaction_spec.rb +71 -0
  49. data/spec/usagelimit_spec.rb +52 -0
  50. data/spec/user_spec.rb +164 -0
  51. data/spec/utilization_spec.rb +113 -0
  52. metadata +182 -0
@@ -0,0 +1,82 @@
1
+ module ThreeScale
2
+ module Core
3
+ class Service < APIClient::Resource
4
+ attributes :provider_key, :id, :backend_version, :referrer_filters_required,
5
+ :user_registration_required, :default_user_plan_id,
6
+ :default_user_plan_name, :default_service
7
+
8
+ class << self
9
+ def load_by_id(service_id)
10
+ api_read({}, uri: service_uri(service_id), rprefix: :service)
11
+ end
12
+
13
+ def delete_by_id!(service_id)
14
+ api_delete({}, uri: service_uri(service_id)) do |result|
15
+ if result[:response].status == 400
16
+ raise ServiceIsDefaultService, service_id
17
+ end
18
+ end
19
+ end
20
+
21
+ def save!(attributes)
22
+ id = attributes.fetch(:id)
23
+ api_update(attributes, uri: service_uri(id)) do |result|
24
+ if result[:response].status == 400
25
+ raise ServiceRequiresDefaultUserPlan
26
+ end
27
+ true
28
+ end
29
+ end
30
+
31
+ def change_provider_key!(old_key, new_key)
32
+ ret = api_do_put({ new_key: new_key },
33
+ uri: "#{default_uri}change_provider_key/#{old_key}",
34
+ prefix: '') do |result|
35
+ if result[:response].status == 400
36
+ exception = provider_key_exception(
37
+ result[:response_json][:error], old_key, new_key)
38
+ raise exception if exception
39
+ end
40
+ true
41
+ end
42
+ ret[:ok]
43
+ end
44
+
45
+ def make_default(service_id)
46
+ save! id: service_id, default_service: true
47
+ end
48
+
49
+ private
50
+
51
+ def service_uri(id)
52
+ "#{default_uri}#{id}"
53
+ end
54
+
55
+ def provider_key_exception(error, old_key, new_key)
56
+ case error
57
+ when /does not exist/
58
+ ProviderKeyNotFound.new old_key
59
+ when /already exists/
60
+ ProviderKeyExists.new new_key
61
+ when /are not valid/
62
+ InvalidProviderKeys.new
63
+ else
64
+ nil
65
+ end
66
+ end
67
+ end
68
+
69
+ def referrer_filters_required?
70
+ @referrer_filters_required
71
+ end
72
+
73
+ def user_registration_required?
74
+ @user_registration_required
75
+ end
76
+
77
+ def save!
78
+ self.class.save! attributes
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,34 @@
1
+ module ThreeScale
2
+ module Core
3
+ class ServiceError < APIClient::Resource
4
+ attributes :code, :message, :timestamp
5
+
6
+ default_uri '/internal/services/'
7
+
8
+ def self.service_errors_uri(service_id)
9
+ "#{default_uri}#{service_id}/errors/"
10
+ end
11
+ private_class_method :service_errors_uri
12
+
13
+ def self.load_all(service_id, options={})
14
+ result = api_do_get(options,
15
+ { uri: service_errors_uri(service_id),
16
+ prefix: '',
17
+ rprefix: :errors }) do |result|
18
+ if result[:response].status == 400 &&
19
+ result[:response_json][:error] == 'per_page needs to be > 0'
20
+ raise InvalidPerPage.new
21
+ end
22
+ true
23
+ end
24
+
25
+ APIClient::Collection.new(result[:attributes].map { |attrs| new attrs },
26
+ result[:response_json][:count])
27
+ end
28
+
29
+ def self.delete_all(service_id)
30
+ api_delete({}, uri: service_errors_uri(service_id))
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,39 @@
1
+ module ThreeScale
2
+ module Core
3
+ class ServiceToken < APIClient::Resource
4
+
5
+ default_uri '/internal/service_tokens/'
6
+
7
+ class << self
8
+
9
+ def save!(attributes)
10
+ api_do_post(attributes, prefix: :service_tokens) do |result|
11
+
12
+ status = result[:response].status
13
+
14
+ if status == 400
15
+ raise ServiceTokenMissingParameter, result[:response_json][:error]
16
+ end
17
+
18
+ if status == 422
19
+ case result[:response_json][:error]
20
+ when /Service ID/
21
+ raise ServiceTokenRequiresServiceId
22
+ when /Service token/
23
+ raise ServiceTokenRequiresToken
24
+ end
25
+ end
26
+
27
+ true
28
+ end
29
+ end
30
+
31
+ def delete(attributes)
32
+ result = api_do_delete(attributes, uri: default_uri, prefix: :service_tokens)
33
+
34
+ result[:response_json][:count]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ module ThreeScale
2
+ module Core
3
+ class Transaction < APIClient::Resource
4
+ attributes :application_id, :usage, :timestamp
5
+
6
+ default_uri '/internal/services/'
7
+
8
+ def self.transactions_uri(service_id)
9
+ "#{default_uri}#{service_id}/transactions/"
10
+ end
11
+ private_class_method :transactions_uri
12
+
13
+ def self.load_all(service_id)
14
+ result = api_do_get({}, { uri: transactions_uri(service_id),
15
+ rprefix: :transactions }) do |result|
16
+ return nil if result[:response].status == 404
17
+ true
18
+ end
19
+
20
+ return nil if result.nil?
21
+
22
+ APIClient::Collection.new(result[:attributes].map { |attrs| new attrs })
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ module ThreeScale
2
+ module Core
3
+ class UsageLimit < APIClient::Resource
4
+ PERIODS = [:eternity, :year, :month, :week, :day, :hour, :minute].freeze
5
+
6
+ attributes :service_id, :plan_id, :metric_id, :value, *PERIODS
7
+
8
+ default_uri '/internal/services/'
9
+
10
+ def self.base_uri(service_id, plan_id, metric_id, period)
11
+ "#{default_uri}#{service_id}/plans/#{plan_id}/usagelimits/#{metric_id}/#{period}"
12
+ end
13
+ private_class_method :base_uri
14
+
15
+ def self.load_value(service_id, plan_id, metric_id, period)
16
+ obj = api_read({}, uri: base_uri(service_id, plan_id, metric_id, period))
17
+ obj and obj.public_send(period).to_i
18
+ end
19
+
20
+ def self.save(attributes)
21
+ # save currently DOES NOT support multiple periods at the same time,
22
+ # since it would mean multiple API calls per call to this method.
23
+ periodlst = PERIODS & attributes.keys
24
+ raise UsageLimitInvalidPeriods.new(periodlst) unless periodlst.one?
25
+
26
+ service_id, plan_id, metric_id = attributes.fetch(:service_id), attributes.fetch(:plan_id), attributes.fetch(:metric_id)
27
+ period = periodlst.shift
28
+ value = attributes[period]
29
+ fixed_fields = { service_id: service_id, plan_id: plan_id, metric_id: metric_id }.freeze
30
+
31
+ api_update(fixed_fields.merge({period => value}), uri: base_uri(service_id, plan_id, metric_id, period))
32
+ end
33
+
34
+ def self.delete(service_id, plan_id, metric_id, period)
35
+ api_delete({}, uri: base_uri(service_id, plan_id, metric_id, period))
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,47 @@
1
+ module ThreeScale
2
+ module Core
3
+ class User < APIClient::Resource
4
+ attributes :service_id, :username, :state, :plan_id, :plan_name
5
+
6
+ default_uri '/internal/services/'
7
+
8
+ def self.base_uri(service_id, username)
9
+ "#{default_uri}#{service_id}/users/#{username}"
10
+ end
11
+ private_class_method :base_uri
12
+
13
+ def self.check_params(service_id, username)
14
+ raise UserRequiresUsername if username.nil? || username == ''.freeze
15
+ raise UserRequiresServiceId if service_id.nil? || service_id == ''.freeze
16
+ end
17
+ private_class_method :check_params
18
+
19
+ def self.load(service_id, username)
20
+ check_params service_id, username
21
+ api_read({}, uri: base_uri(service_id, username))
22
+ end
23
+
24
+ def self.save!(attributes)
25
+ service_id, username = attributes[:service_id], attributes[:username]
26
+ check_params service_id, username
27
+ api_update(attributes,
28
+ uri: base_uri(service_id, username)) do |result|
29
+ if result[:response].status == 400
30
+ if result[:response_json][:error] =~ /requires a valid service/
31
+ raise UserRequiresValidServiceId.new(service_id)
32
+ elsif result[:response_json][:error] =~ /requires a defined plan/
33
+ raise UserRequiresDefinedPlan.new(attributes[:plan_id],
34
+ attributes[:plan_name])
35
+ end
36
+ end
37
+ true
38
+ end
39
+ end
40
+
41
+ def self.delete!(service_id, username)
42
+ check_params service_id, username
43
+ api_delete({}, uri: base_uri(service_id, username))
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,28 @@
1
+ module ThreeScale
2
+ module Core
3
+ class Utilization < APIClient::Resource
4
+ attributes :period, :metric_name, :max_value, :current_value
5
+
6
+ default_uri '/internal/services/'
7
+
8
+ def self.utilization_uri(service_id, app_id)
9
+ "#{default_uri}#{service_id}/applications/#{app_id}/utilization/"
10
+ end
11
+ private_class_method :utilization_uri
12
+
13
+ def self.load(service_id, app_id)
14
+ result = api_do_get({},
15
+ uri: utilization_uri(service_id, app_id),
16
+ rprefix: :utilization) do |result|
17
+ return nil if result[:response].status == 404
18
+ true
19
+ end
20
+
21
+ return nil if result.nil?
22
+
23
+ usage_reports = result[:attributes].map { |attrs| new attrs }
24
+ APIClient::Collection.new(usage_reports)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ module ThreeScale
2
+ module Core
3
+ VERSION = '1.23.1'
4
+ end
5
+ end
@@ -0,0 +1,63 @@
1
+ require 'uri'
2
+ require 'json'
3
+ require 'faraday'
4
+
5
+ require '3scale/core/version'
6
+ require '3scale/core/logger'
7
+
8
+ require '3scale/core/api_client'
9
+ require '3scale/core/application'
10
+ require '3scale/core/metric'
11
+ require '3scale/core/service'
12
+ require '3scale/core/usage_limit'
13
+ require '3scale/core/user'
14
+ require '3scale/core/event'
15
+ require '3scale/core/alert_limit'
16
+ require '3scale/core/errors'
17
+ require '3scale/core/application_key'
18
+ require '3scale/core/application_referrer_filter'
19
+ require '3scale/core/service_error'
20
+ require '3scale/core/transaction'
21
+ require '3scale/core/utilization'
22
+ require '3scale/core/service_token'
23
+
24
+ module ThreeScale
25
+ module Core
26
+ extend self
27
+
28
+ attr_accessor :username, :password
29
+ attr_writer :url
30
+
31
+ def faraday
32
+ return @faraday if @faraday
33
+
34
+ url = self.url
35
+ @faraday = Faraday.new(url: url) do |f|
36
+ f.adapter :net_http_persistent
37
+ end
38
+ @faraday.headers = {
39
+ 'User-Agent' => "pisoni v#{ThreeScale::Core::VERSION}",
40
+ 'Accept' => 'application/json',
41
+ 'Content-Type' => 'application/json'
42
+ }
43
+
44
+ if @username.nil? && @password.nil?
45
+ # even though the url may contain the user info, turns out Faraday is
46
+ # not really picking it up, so must fill it in if present in the URL and
47
+ # no previous setting was done (ie. assigning username or password).
48
+ uri = URI.parse url
49
+ @username = uri.user
50
+ @password = uri.password
51
+ end
52
+
53
+ @faraday.basic_auth(@username, @password) if @username || @password
54
+ @faraday
55
+ end
56
+
57
+ def url
58
+ ENV['THREESCALE_CORE_INTERNAL_API'] || @url ||
59
+ raise(UnknownAPIEndpoint)
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1 @@
1
+ require '3scale/core'
data/lib/pisoni.rb ADDED
@@ -0,0 +1,5 @@
1
+ require '3scale/core'
2
+
3
+ module Pisoni
4
+ include ThreeScale::Core
5
+ end
data/pisoni.gemspec ADDED
@@ -0,0 +1,43 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require '3scale/core/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'pisoni'
8
+ s.version = ThreeScale::Core::VERSION
9
+ s.date = Time.now.utc.strftime('%Y-%m-%d')
10
+
11
+ s.platform = Gem::Platform::RUBY
12
+
13
+ s.authors = ['Alejandro Martinez Ruiz']
14
+ s.email = %w[alex@3scale.net]
15
+
16
+ s.homepage = 'https://github.com/3scale/pisoni'
17
+ s.summary = 'Client for the Apisonator internal API for model data'
18
+ s.description = 'Client for the Apisonator internal API for model data.'
19
+ s.license = 'Apache-2.0'
20
+
21
+ s.add_dependency 'faraday', '~> 0.13.1'
22
+ s.add_dependency 'json', '~> 1.8.1'
23
+ s.add_dependency 'injectedlogger', '~> 0.0.13'
24
+ s.add_dependency 'net-http-persistent'
25
+
26
+ s.add_development_dependency 'rake'
27
+
28
+ s.files = `git ls-files`.split($/).reject do |f| [
29
+ %r{^\.[^\/]},
30
+ %r{^script/},
31
+ %r{^docker/},
32
+ %r{^mk/},
33
+ ].any? { |r| r.match f }
34
+ end
35
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
36
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
37
+
38
+ s.require_paths = ["lib"]
39
+
40
+ s.rdoc_options = ["--charset=UTF-8"]
41
+
42
+ s.required_ruby_version = '>= 2.2.4'
43
+ end
@@ -0,0 +1,72 @@
1
+ require_relative './spec_helper'
2
+ module ThreeScale
3
+ module Core
4
+ describe AlertLimit do
5
+ describe '.load_all' do
6
+ describe 'when there are alert limits' do
7
+ let(:service_id) { 100 }
8
+ let(:values) { [50, 100] }
9
+ before do
10
+ values.map { |value| AlertLimit.delete(service_id, value) }
11
+ values.map { |value| AlertLimit.save(service_id, value) }
12
+ end
13
+
14
+ it 'returns a list of alert limits' do
15
+ alert_limits = AlertLimit.load_all(service_id)
16
+
17
+ alert_limits.size.must_equal 2
18
+ alert_limits.map(&:value).must_equal values
19
+ end
20
+ end
21
+
22
+ describe 'when there are no alert limits' do
23
+ let(:service_id) { 200 }
24
+
25
+ it 'returns an empty list' do
26
+ AlertLimit.load_all(service_id).must_equal []
27
+ end
28
+ end
29
+ end
30
+
31
+ describe '.save' do
32
+ let(:service_id) { 500 }
33
+ let(:value) { 100 }
34
+
35
+ before do
36
+ AlertLimit.delete(service_id, value)
37
+ end
38
+
39
+ it 'returns a AlertLimit object' do
40
+ alert_limit = AlertLimit.save(service_id, value)
41
+
42
+ alert_limit.must_be_kind_of AlertLimit
43
+ alert_limit.value.must_equal value
44
+ end
45
+ end
46
+
47
+ describe '.delete' do
48
+ describe 'with an existing alert limit' do
49
+ let(:service_id) { 300 }
50
+ let(:value) { 50 }
51
+
52
+ before do
53
+ AlertLimit.save(service_id, value)
54
+ end
55
+
56
+ it 'returns true' do
57
+ AlertLimit.delete(service_id, value).must_equal true
58
+ end
59
+ end
60
+
61
+ describe 'with a non-existing alert limit' do
62
+ let(:service_id) { 300 }
63
+ let(:value) { 75 }
64
+
65
+ it 'returns true' do
66
+ AlertLimit.delete(service_id, value).must_equal false
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,97 @@
1
+ require_relative './spec_helper'
2
+ module ThreeScale
3
+ module Core
4
+ describe ApplicationKey do
5
+ describe '.load_all' do
6
+ describe 'when there are application keys' do
7
+ let(:service_id) { 100 }
8
+ let(:app_id) { 2001 }
9
+ let(:values) { ["foo", "bar"] }
10
+ before do
11
+ values.map { |value| ApplicationKey.delete(service_id, app_id, value) }
12
+
13
+ Application.save service_id: service_id, id: app_id, state: 'suspended',
14
+ plan_id: '3066', plan_name: 'crappy', redirect_url: 'blah'
15
+
16
+ values.map { |value| ApplicationKey.save(service_id, app_id, value) }
17
+ end
18
+
19
+ it 'returns a list of application keys' do
20
+ application_keys = ApplicationKey.load_all(service_id, app_id)
21
+
22
+ application_keys.size.must_equal 2
23
+ application_keys.map(&:value).sort.must_equal values.sort
24
+ end
25
+ end
26
+
27
+ describe 'when there are no application keys' do
28
+ let(:service_id) { 200 }
29
+ let(:app_id) { 300 }
30
+
31
+ before do
32
+ Application.save service_id: service_id, id: app_id, state: 'suspended',
33
+ plan_id: '3066', plan_name: 'crappy', redirect_url: 'blah'
34
+ end
35
+
36
+ it 'returns an empty list' do
37
+ ApplicationKey.load_all(service_id, app_id).must_equal []
38
+ end
39
+ end
40
+ end
41
+
42
+ describe '.save' do
43
+ let(:service_id) { 500 }
44
+ let(:app_id) { 500 }
45
+ let(:value) { "foobar" }
46
+
47
+ before do
48
+ ApplicationKey.delete(service_id, app_id, value)
49
+
50
+ Application.save service_id: service_id, id: app_id, state: 'suspended',
51
+ plan_id: '3066', plan_name: 'crappy', redirect_url: 'blah'
52
+ end
53
+
54
+ it 'returns an ApplicationKey object' do
55
+ application_key = ApplicationKey.save(service_id, app_id, value)
56
+
57
+ application_key.must_be_kind_of ApplicationKey
58
+ application_key.value.must_equal value
59
+ end
60
+ end
61
+
62
+ describe '.delete' do
63
+ describe 'with an existing application key' do
64
+ let(:service_id) { 300 }
65
+ let(:app_id) { 200 }
66
+ let(:value) { "foo" }
67
+
68
+ before do
69
+ Application.save service_id: service_id, id: app_id, state: 'suspended',
70
+ plan_id: '3066', plan_name: 'crappy', redirect_url: 'blah'
71
+
72
+ ApplicationKey.save(service_id, app_id, value)
73
+ end
74
+
75
+ it 'returns true' do
76
+ ApplicationKey.delete(service_id, app_id, value).must_equal true
77
+ end
78
+ end
79
+
80
+ describe 'with a non-existing application key' do
81
+ let(:service_id) { 300 }
82
+ let(:app_id) { 500 }
83
+ let(:value) { "nonexistingkey" }
84
+
85
+ before do
86
+ Application.save service_id: service_id, id: app_id, state: 'suspended',
87
+ plan_id: '3066', plan_name: 'crappy', redirect_url: 'blah'
88
+ end
89
+
90
+ it 'returns true' do
91
+ ApplicationKey.delete(service_id, app_id, value).must_equal false
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,57 @@
1
+ require_relative './spec_helper'
2
+ module ThreeScale
3
+ module Core
4
+ describe ApplicationReferrerFilter do
5
+ let(:service_id) { 10 }
6
+ let(:app_id) { 100 }
7
+ let(:filters) { %w(foo bar doopah) }
8
+ let(:application) do
9
+ { service_id: service_id,
10
+ id: app_id,
11
+ state: 'suspended',
12
+ plan_id: '3066',
13
+ plan_name: 'crappy',
14
+ redirect_url: 'blah' }
15
+ end
16
+
17
+ before do
18
+ filters.map do |filter|
19
+ ApplicationReferrerFilter.delete(service_id, app_id, filter)
20
+ end
21
+
22
+ Application.delete(service_id, app_id)
23
+ Application.save(application)
24
+ end
25
+
26
+ describe '.load_all' do
27
+ describe 'Getting all referrer filters' do
28
+ let(:values) { %w(foo bar) }
29
+
30
+ before do
31
+ values.map { |value| ApplicationReferrerFilter.save(service_id, app_id, value) }
32
+ end
33
+
34
+ it 'returns a sorted list of filters' do
35
+ filters = ApplicationReferrerFilter.load_all(service_id, app_id)
36
+ filters.must_equal values.sort
37
+ end
38
+ end
39
+
40
+ describe 'when there are no referrer filters' do
41
+ it 'returns an empty list' do
42
+ ApplicationReferrerFilter.load_all(service_id, app_id).must_equal []
43
+ end
44
+ end
45
+ end
46
+
47
+ describe '.save' do
48
+ let(:filter) { 'doopah' }
49
+
50
+ it 'saves the filter' do
51
+ ApplicationReferrerFilter.save(service_id, app_id, filter)
52
+ ApplicationReferrerFilter.load_all(service_id, app_id).must_equal([filter])
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end