guillotine 1.0.8 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +12 -2
- data/guillotine.gemspec +9 -2
- data/lib/guillotine/adapters/active_record_adapter.rb +10 -8
- data/lib/guillotine/adapters/mongo_adapter.rb +70 -0
- data/lib/guillotine/adapters/redis_adapter.rb +63 -0
- data/lib/guillotine/adapters/sequel_adapter.rb +10 -8
- data/lib/guillotine/app.rb +12 -30
- data/lib/guillotine/service.rb +132 -0
- data/lib/guillotine.rb +4 -1
- data/script/cibuild +2 -0
- data/test/active_record_adapter_test.rb +13 -13
- data/test/app_test.rb +119 -115
- data/test/mongo_adapter_test.rb +56 -0
- data/test/redis_adapter_test.rb +56 -0
- data/test/riak_adapter_test.rb +8 -3
- data/test/service_test.rb +115 -0
- metadata +22 -9
data/Gemfile
CHANGED
@@ -1,12 +1,22 @@
|
|
1
1
|
source 'http://rubygems.org'
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
gem 'rake'
|
5
6
|
|
6
7
|
group :test do
|
7
8
|
gem 'rack-test'
|
8
9
|
end
|
9
10
|
|
11
|
+
if ENV['TRAVIS']
|
12
|
+
gem 'sequel'
|
13
|
+
gem 'sqlite3'
|
14
|
+
gem 'redis'
|
15
|
+
gem 'mongo'
|
16
|
+
gem 'bson_ext'
|
17
|
+
gem 'riak-client'
|
18
|
+
end
|
19
|
+
|
10
20
|
# Bundler isn't designed to provide optional functionality like this. You're
|
11
21
|
# on your own
|
12
22
|
#
|
data/guillotine.gemspec
CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
|
|
13
13
|
## If your rubyforge_project name is different, then edit it and comment out
|
14
14
|
## the sub! line in the Rakefile
|
15
15
|
s.name = 'guillotine'
|
16
|
-
s.version = '1.0
|
17
|
-
s.date = '2011-11-
|
16
|
+
s.version = '1.1.0'
|
17
|
+
s.date = '2011-11-26'
|
18
18
|
s.rubyforge_project = 'guillotine'
|
19
19
|
|
20
20
|
## Make sure your summary is short. The description may be as long
|
@@ -56,15 +56,22 @@ Gem::Specification.new do |s|
|
|
56
56
|
lib/guillotine.rb
|
57
57
|
lib/guillotine/adapters/active_record_adapter.rb
|
58
58
|
lib/guillotine/adapters/memory_adapter.rb
|
59
|
+
lib/guillotine/adapters/mongo_adapter.rb
|
60
|
+
lib/guillotine/adapters/redis_adapter.rb
|
59
61
|
lib/guillotine/adapters/riak_adapter.rb
|
60
62
|
lib/guillotine/adapters/sequel_adapter.rb
|
61
63
|
lib/guillotine/app.rb
|
64
|
+
lib/guillotine/service.rb
|
65
|
+
script/cibuild
|
62
66
|
test/active_record_adapter_test.rb
|
63
67
|
test/app_test.rb
|
64
68
|
test/helper.rb
|
65
69
|
test/memory_adapter_test.rb
|
70
|
+
test/mongo_adapter_test.rb
|
71
|
+
test/redis_adapter_test.rb
|
66
72
|
test/riak_adapter_test.rb
|
67
73
|
test/sequel_adapter_test.rb
|
74
|
+
test/service_test.rb
|
68
75
|
]
|
69
76
|
# = MANIFEST =
|
70
77
|
|
@@ -23,7 +23,7 @@ module Guillotine
|
|
23
23
|
code ||= shorten url
|
24
24
|
begin
|
25
25
|
Url.create :url => url, :code => code
|
26
|
-
rescue ActiveRecord::RecordNotUnique
|
26
|
+
rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid
|
27
27
|
row = Url.select(:url).where(:code => code).first
|
28
28
|
existing_url = row && row[:url]
|
29
29
|
raise DuplicateCodeError.new(existing_url, url, code)
|
@@ -38,9 +38,7 @@ module Guillotine
|
|
38
38
|
#
|
39
39
|
# Returns the String URL.
|
40
40
|
def find(code)
|
41
|
-
|
42
|
-
row[:url]
|
43
|
-
end
|
41
|
+
select :url, :code => code
|
44
42
|
end
|
45
43
|
|
46
44
|
# Public: Retrieves the code for a given URL.
|
@@ -49,9 +47,7 @@ module Guillotine
|
|
49
47
|
#
|
50
48
|
# Returns the String code, or nil if none is found.
|
51
49
|
def code_for(url)
|
52
|
-
|
53
|
-
row[:code]
|
54
|
-
end
|
50
|
+
select :code, :url => url
|
55
51
|
end
|
56
52
|
|
57
53
|
# Public: Removes the assigned short code for a URL.
|
@@ -60,7 +56,7 @@ module Guillotine
|
|
60
56
|
#
|
61
57
|
# Returns nothing.
|
62
58
|
def clear(url)
|
63
|
-
Url.where(:url => url).
|
59
|
+
Url.where(:url => url).delete_all
|
64
60
|
end
|
65
61
|
|
66
62
|
def setup
|
@@ -73,6 +69,12 @@ module Guillotine
|
|
73
69
|
conn.add_index :urls, :url, :unique => true
|
74
70
|
conn.add_index :urls, :code, :unique => true
|
75
71
|
end
|
72
|
+
|
73
|
+
def select(field, query)
|
74
|
+
if row = Url.select(field).where(query).first
|
75
|
+
row[field]
|
76
|
+
end
|
77
|
+
end
|
76
78
|
end
|
77
79
|
end
|
78
80
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'mongo'
|
2
|
+
|
3
|
+
module Guillotine
|
4
|
+
module Adapters
|
5
|
+
class MongoAdapter < Adapter
|
6
|
+
def initialize(collection)
|
7
|
+
@collection = collection
|
8
|
+
@collection.ensure_index([['url', Mongo::ASCENDING]])
|
9
|
+
|
10
|
+
# \m/
|
11
|
+
@transformers = {
|
12
|
+
:url => lambda { |doc| doc['url'] },
|
13
|
+
:code => lambda { |doc| doc['_id'] }
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Public: Stores the shortened version of a URL.
|
18
|
+
#
|
19
|
+
# url - The String URL to shorten and store.
|
20
|
+
# code - Optional String code for the URL.
|
21
|
+
#
|
22
|
+
# Returns the unique String code for the URL. If the URL is added
|
23
|
+
# multiple times, this should return the same code.
|
24
|
+
def add(url, code = nil)
|
25
|
+
code_for(url) || insert(url, code || shorten(url))
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# Public: Retrieves a URL from the code.
|
30
|
+
#
|
31
|
+
# code - The String code to lookup the URL.
|
32
|
+
#
|
33
|
+
# Returns the String URL, or nil if none is found.
|
34
|
+
def find(code)
|
35
|
+
select :url, :_id => code
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Retrieves the code for a given URL.
|
39
|
+
#
|
40
|
+
# url - The String URL to lookup.
|
41
|
+
#
|
42
|
+
# Returns the String code, or nil if none is found.
|
43
|
+
def code_for(url)
|
44
|
+
select :code, :url => url
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: Removes the assigned short code for a URL.
|
48
|
+
#
|
49
|
+
# url - The String URL to remove.
|
50
|
+
#
|
51
|
+
# Returns nothing.
|
52
|
+
def clear(url)
|
53
|
+
@collection.remove(:url => url)
|
54
|
+
end
|
55
|
+
|
56
|
+
def select(field, query)
|
57
|
+
@collection.find_one(query, {:transformer => @transformers[field]})
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def insert(url, code)
|
62
|
+
if existing_url = find(code)
|
63
|
+
raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
|
64
|
+
end
|
65
|
+
@collection.insert(:_id => code, :url => url)
|
66
|
+
code
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Guillotine
|
2
|
+
module Adapters
|
3
|
+
class RedisAdapter < Adapter
|
4
|
+
# Public: Initialise the adapter with a Redis instance.
|
5
|
+
#
|
6
|
+
# redis - A Redis instance to persist urls and codes to.
|
7
|
+
def initialize(redis)
|
8
|
+
@redis = redis
|
9
|
+
end
|
10
|
+
|
11
|
+
# Public: Stores the shortened version of a URL.
|
12
|
+
#
|
13
|
+
# url - The String URL to shorten and store.
|
14
|
+
# code - Optional String code for the URL.
|
15
|
+
#
|
16
|
+
# Returns the unique String code for the URL. If the URL is added
|
17
|
+
# multiple times, this should return the same code.
|
18
|
+
def add(url, code = nil)
|
19
|
+
if existing_code = @redis.get("guillotine:urls:#{url}")
|
20
|
+
existing_code
|
21
|
+
else
|
22
|
+
code ||= shorten(url)
|
23
|
+
if existing_url = @redis.get("guillotine:hash:#{code}")
|
24
|
+
raise DuplicateCodeError.new(existing_url, url, code) if url != existing_url
|
25
|
+
end
|
26
|
+
@redis.set "guillotine:hash:#{code}", url
|
27
|
+
@redis.set "guillotine:urls:#{url}", code
|
28
|
+
code
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Public: Retrieves a URL from the code.
|
33
|
+
#
|
34
|
+
# code - The String code to lookup the URL.
|
35
|
+
#
|
36
|
+
# Returns the String URL, or nil if none is found.
|
37
|
+
def find(code)
|
38
|
+
@redis.get "guillotine:hash:#{code}"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Public: Retrieves the code for a given URL.
|
42
|
+
#
|
43
|
+
# url - The String URL to lookup.
|
44
|
+
#
|
45
|
+
# Returns the String code, or nil if none is found.
|
46
|
+
def code_for(url)
|
47
|
+
@redis.get "guillotine:urls:#{url}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Public: Removes the assigned short code for a URL.
|
51
|
+
#
|
52
|
+
# url - The String URL to remove.
|
53
|
+
#
|
54
|
+
# Returns nothing.
|
55
|
+
def clear(url)
|
56
|
+
if code = @redis.get("guillotine:urls:#{url}")
|
57
|
+
@redis.del "guillotine:urls:#{url}"
|
58
|
+
@redis.del "guillotine:hash:#{code}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -37,9 +37,7 @@ module Guillotine
|
|
37
37
|
#
|
38
38
|
# Returns the String URL.
|
39
39
|
def find(code)
|
40
|
-
|
41
|
-
row[:url]
|
42
|
-
end
|
40
|
+
select :url, :code => code
|
43
41
|
end
|
44
42
|
|
45
43
|
# Public: Retrieves the code for a given URL.
|
@@ -48,9 +46,7 @@ module Guillotine
|
|
48
46
|
#
|
49
47
|
# Returns the String code, or nil if none is found.
|
50
48
|
def code_for(url)
|
51
|
-
|
52
|
-
row[:code]
|
53
|
-
end
|
49
|
+
select :code, :url => url
|
54
50
|
end
|
55
51
|
|
56
52
|
# Public: Removes the assigned short code for a URL.
|
@@ -64,13 +60,19 @@ module Guillotine
|
|
64
60
|
|
65
61
|
def setup
|
66
62
|
@db.create_table :urls do
|
67
|
-
|
68
|
-
|
63
|
+
String :url
|
64
|
+
String :code
|
69
65
|
|
70
66
|
unique :url
|
71
67
|
unique :code
|
72
68
|
end
|
73
69
|
end
|
70
|
+
|
71
|
+
def select(field, query)
|
72
|
+
if row = @table.select(field).where(query).first
|
73
|
+
row[field]
|
74
|
+
end
|
75
|
+
end
|
74
76
|
end
|
75
77
|
end
|
76
78
|
end
|
data/lib/guillotine/app.rb
CHANGED
@@ -1,53 +1,35 @@
|
|
1
1
|
require 'sinatra/base'
|
2
2
|
|
3
3
|
module Guillotine
|
4
|
+
# Essentially herds Sinatra input to Guillotine::Service, and ensures the
|
5
|
+
# output is fit Sinatra to return.
|
4
6
|
class App < Sinatra::Base
|
5
|
-
set :
|
7
|
+
set :service, nil
|
6
8
|
|
7
9
|
get "/:code" do
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
else
|
12
|
-
halt 404, simple_escape("No url found for #{code}")
|
13
|
-
end
|
10
|
+
escaped = Addressable::URI.escape(params[:code])
|
11
|
+
status, head, body = settings.service.get(escaped)
|
12
|
+
[status, head, simple_escape(body)]
|
14
13
|
end
|
15
14
|
|
16
15
|
post "/" do
|
17
|
-
|
18
|
-
|
19
|
-
if !(url && url.scheme =~ /^https?$/)
|
20
|
-
halt 422, simple_escape("Invalid url: #{url}")
|
21
|
-
end
|
16
|
+
status, head, body = settings.service.create(params[:url], params[:code])
|
22
17
|
|
23
|
-
|
24
|
-
|
25
|
-
if url.host != settings.required_host
|
26
|
-
halt 422, simple_escape("URL must be from #{settings.required_host}")
|
27
|
-
end
|
28
|
-
when Regexp
|
29
|
-
if url.host.to_s !~ settings.required_host
|
30
|
-
halt 422, simple_escape("URL must match #{settings.required_host.inspect}")
|
31
|
-
end
|
18
|
+
if loc = head['Location']
|
19
|
+
head['Location'] = File.join(request.url, loc)
|
32
20
|
end
|
33
21
|
|
34
|
-
|
35
|
-
if code = settings.db.add(url.to_s, params[:code])
|
36
|
-
redirect code, 201
|
37
|
-
else
|
38
|
-
halt 422, simple_escape("Unable to shorten #{url}")
|
39
|
-
end
|
40
|
-
rescue Guillotine::DuplicateCodeError => err
|
41
|
-
halt 422, simple_escape(err.to_s)
|
42
|
-
end
|
22
|
+
[status, head, simple_escape(body)]
|
43
23
|
end
|
44
24
|
|
45
25
|
# Guillotine output is supposed to be text/plain friendly, so only strip
|
46
26
|
# /<|>/. Broken tie fighter :( If you're passing these characters in,
|
47
27
|
# you're probably doing something naughty.
|
48
28
|
def simple_escape(s)
|
29
|
+
s = s.to_s
|
49
30
|
s.gsub! /<|>/, ''
|
50
31
|
s
|
51
32
|
end
|
52
33
|
end
|
53
34
|
end
|
35
|
+
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Guillotine
|
2
|
+
class Service
|
3
|
+
class NullChecker
|
4
|
+
def call(url)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
# This is the public API to the Guillotine service. Wire this up to Sinatra
|
9
|
+
# or whatever. Every public method should return a compatible Rack Response:
|
10
|
+
# [Integer Status, Hash headers, String body].
|
11
|
+
#
|
12
|
+
# db - A Guillotine::Adapter instance.
|
13
|
+
# required_host - Either a String or Regex limiting which domains the
|
14
|
+
# shortened URLs can come from.
|
15
|
+
#
|
16
|
+
def initialize(db, required_host = nil)
|
17
|
+
@db = db
|
18
|
+
build_host_check(required_host)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Gets the full URL for a shortened code.
|
22
|
+
#
|
23
|
+
# code - A String short code.
|
24
|
+
#
|
25
|
+
# Returns 302 with the Location header pointing to the URL on a hit,
|
26
|
+
# or 404 on a miss.
|
27
|
+
def get(code)
|
28
|
+
if url = @db.find(code)
|
29
|
+
[302, {"Location" => @db.parse_url(url).to_s}]
|
30
|
+
else
|
31
|
+
[404, {}, "No url found for #{code}"]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Public: Maps a URL to a shortened code.
|
36
|
+
#
|
37
|
+
# url - A String or Addressable::URI URL to shorten.
|
38
|
+
# code - Optional String code to use. Defaults to a random String.
|
39
|
+
#
|
40
|
+
# Returns 201 with the Location pointing to the code, or 422.
|
41
|
+
def create(url, code = nil)
|
42
|
+
url = ensure_url(url)
|
43
|
+
|
44
|
+
if resp = check_host(url)
|
45
|
+
return resp
|
46
|
+
end
|
47
|
+
|
48
|
+
begin
|
49
|
+
if code = @db.add(url.to_s, code)
|
50
|
+
[201, {"Location" => code}]
|
51
|
+
else
|
52
|
+
[422, {}, "Unable to shorten #{url}"]
|
53
|
+
end
|
54
|
+
rescue DuplicateCodeError => err
|
55
|
+
[422, {}, err.to_s]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Checks to see if the input URL is using a valid host. You can constrain
|
60
|
+
# the hosts with the `required_host` argument of the Service constructor.
|
61
|
+
#
|
62
|
+
# url - An Addressible::URI instance to check.
|
63
|
+
#
|
64
|
+
# Returns a 422 Rack::Response if the host is invalid, or nil.
|
65
|
+
def check_host(url)
|
66
|
+
if url.scheme !~ /^https?$/
|
67
|
+
[422, {}, "Invalid url: #{url}"]
|
68
|
+
else
|
69
|
+
@host_check.call url
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Converts the `required_host` argument to a lambda for #check_host.
|
74
|
+
#
|
75
|
+
# host_check - Either a String or Regex limiting which domains the
|
76
|
+
# shortened URLs can come from.
|
77
|
+
#
|
78
|
+
# Returns nothing.
|
79
|
+
def build_host_check(host_check)
|
80
|
+
case host_check
|
81
|
+
when nil
|
82
|
+
@host_check = NullChecker.new
|
83
|
+
when Regexp
|
84
|
+
build_host_regex_check(host_check)
|
85
|
+
else
|
86
|
+
build_host_string_check(host_check.to_s)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Builds the host check lambda for regexes.
|
91
|
+
#
|
92
|
+
# regex - The Regexp that limits allowable URL hosts.
|
93
|
+
#
|
94
|
+
# Returns a Lambda that verifies an Addressible::URI.
|
95
|
+
def build_host_regex_check(regex)
|
96
|
+
@host_check = lambda do |url|
|
97
|
+
if url.host.to_s !~ regex
|
98
|
+
[422, {}, "URL must match #{regex.inspect}"]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Builds the host check lambda for Strings.
|
104
|
+
#
|
105
|
+
# hostname - The String that limits allowable URL hosts.
|
106
|
+
#
|
107
|
+
# Returns a Lambda that verifies an Addressible::URI.
|
108
|
+
def build_host_string_check(hostname)
|
109
|
+
@host_check = lambda do |url|
|
110
|
+
if url.host != hostname
|
111
|
+
[422, {}, "URL must be from #{hostname}"]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Ensures that the argument is an Addressable::URI.
|
117
|
+
#
|
118
|
+
# str - A String URL or an Addressable::URI.
|
119
|
+
#
|
120
|
+
# Returns an Addressable::URI.
|
121
|
+
def ensure_url(str)
|
122
|
+
if str.respond_to?(:scheme)
|
123
|
+
str
|
124
|
+
else
|
125
|
+
str = str.to_s
|
126
|
+
str.gsub! /\s/, ''
|
127
|
+
str.gsub! /(\#|\?).*/, ''
|
128
|
+
Addressable::URI.parse str
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/guillotine.rb
CHANGED
@@ -3,9 +3,10 @@ require 'digest/md5'
|
|
3
3
|
require 'addressable/uri'
|
4
4
|
|
5
5
|
module Guillotine
|
6
|
-
VERSION = "1.0
|
6
|
+
VERSION = "1.1.0"
|
7
7
|
|
8
8
|
dir = File.expand_path '../guillotine', __FILE__
|
9
|
+
require "#{dir}/service"
|
9
10
|
autoload :App, "#{dir}/app"
|
10
11
|
|
11
12
|
class DuplicateCodeError < StandardError
|
@@ -25,6 +26,8 @@ module Guillotine
|
|
25
26
|
autoload :SequelAdapter, "#{dir}/sequel_adapter"
|
26
27
|
autoload :RiakAdapter, "#{dir}/riak_adapter"
|
27
28
|
autoload :ActiveRecordAdapter, "#{dir}/active_record_adapter"
|
29
|
+
autoload :RedisAdapter, "#{dir}/redis_adapter"
|
30
|
+
autoload :MongoAdapter, "#{dir}/mongo_adapter"
|
28
31
|
|
29
32
|
# Adapters handle the storage and retrieval of URLs in the system. You can
|
30
33
|
# use whatever you want, as long as it implements the #add and #find
|
data/script/cibuild
ADDED
@@ -11,24 +11,24 @@ begin
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def test_adding_a_link_returns_code
|
14
|
-
code = @db.add '
|
15
|
-
assert_equal '
|
14
|
+
code = @db.add 'aaa'
|
15
|
+
assert_equal 'aaa', @db.find(code)
|
16
16
|
end
|
17
17
|
|
18
18
|
def test_adding_duplicate_link_returns_same_code
|
19
|
-
code = @db.add '
|
20
|
-
assert_equal code, @db.add('
|
19
|
+
code = @db.add 'bbb'
|
20
|
+
assert_equal code, @db.add('bbb')
|
21
21
|
end
|
22
22
|
|
23
23
|
def test_adds_url_with_custom_code
|
24
|
-
assert_equal 'code', @db.add('
|
25
|
-
assert_equal '
|
24
|
+
assert_equal 'code', @db.add('ccc', 'code')
|
25
|
+
assert_equal 'ccc', @db.find('code')
|
26
26
|
end
|
27
27
|
|
28
28
|
def test_clashing_urls_raises_error
|
29
|
-
code = @db.add '
|
29
|
+
code = @db.add 'ddd'
|
30
30
|
assert_raises Guillotine::DuplicateCodeError do
|
31
|
-
code = @db.add '
|
31
|
+
code = @db.add 'eee', code
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
@@ -37,15 +37,15 @@ begin
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def test_gets_code_for_url
|
40
|
-
code = @db.add '
|
41
|
-
assert_equal code, @db.code_for('
|
40
|
+
code = @db.add 'fff'
|
41
|
+
assert_equal code, @db.code_for('fff')
|
42
42
|
end
|
43
43
|
|
44
44
|
def test_clears_code_for_url
|
45
|
-
code = @db.add '
|
46
|
-
assert_equal '
|
45
|
+
code = @db.add 'ggg'
|
46
|
+
assert_equal 'ggg', @db.find(code)
|
47
47
|
|
48
|
-
@db.clear '
|
48
|
+
@db.clear 'ggg'
|
49
49
|
|
50
50
|
assert_nil @db.find(code)
|
51
51
|
end
|
data/test/app_test.rb
CHANGED
@@ -1,119 +1,123 @@
|
|
1
1
|
require File.expand_path('../helper', __FILE__)
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
3
|
+
module Guillotine
|
4
|
+
class AppTest < TestCase
|
5
|
+
ADAPTER = Adapters::MemoryAdapter.new
|
6
|
+
SERVICE = Service.new(ADAPTER)
|
7
|
+
App.set :service, SERVICE
|
8
|
+
|
9
|
+
include Rack::Test::Methods
|
10
|
+
|
11
|
+
def test_adding_a_link_returns_code
|
12
|
+
url = 'http://github.com'
|
13
|
+
post '/', :url => url + '?a=1'
|
14
|
+
assert_equal 201, last_response.status
|
15
|
+
assert code_url = last_response.headers['Location']
|
16
|
+
code = code_url.gsub(/.*\//, '')
|
17
|
+
|
18
|
+
get "/#{code}"
|
19
|
+
assert_equal 302, last_response.status
|
20
|
+
assert_equal url, last_response.headers['Location']
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_adding_duplicate_link_returns_same_code
|
24
|
+
url = 'http://github.com'
|
25
|
+
code = ADAPTER.add url
|
26
|
+
|
27
|
+
post '/', :url => url + '#a=1'
|
28
|
+
assert code_url = last_response.headers['Location']
|
29
|
+
assert_equal code, code_url.gsub(/.*\//, '')
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_adds_url_with_custom_code
|
33
|
+
url = 'http://github.com/abc'
|
34
|
+
post '/', :url => url, :code => 'code'
|
35
|
+
assert code_url = last_response.headers['Location']
|
36
|
+
assert_match /\/code$/, code_url
|
37
|
+
|
38
|
+
get "/code"
|
39
|
+
assert_equal 302, last_response.status
|
40
|
+
assert_equal url, last_response.headers['Location']
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_adds_url_with_custom_code
|
44
|
+
url = 'http://github.com/abc'
|
45
|
+
post '/', :url => url, :code => '%E2%9C%88'
|
46
|
+
assert code_url = last_response.headers['Location']
|
47
|
+
assert_match /\/%E2%9C%88$/, code_url
|
48
|
+
|
49
|
+
get "/%E2%9C%88"
|
50
|
+
assert_equal 302, last_response.status
|
51
|
+
assert_equal url, last_response.headers['Location']
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_redirects_to_split_url
|
55
|
+
url = "http://abc.com\nhttp//def.com"
|
56
|
+
ADAPTER.hash['split'] = url
|
57
|
+
ADAPTER.urls[url] = 'split'
|
58
|
+
|
59
|
+
get '/split'
|
60
|
+
assert_equal "http://abc.comhttp//def.com", last_response.headers['location']
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_clashing_urls_raises_error
|
64
|
+
code = ADAPTER.add 'http://github.com/123'
|
65
|
+
post '/', :url => 'http://github.com/456', :code => code
|
66
|
+
assert_equal 422, last_response.status
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_add_without_url
|
70
|
+
post '/'
|
71
|
+
assert_equal 422, last_response.status
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_add_url_with_linebreak
|
75
|
+
post '/', :url => "https://abc.com\n"
|
76
|
+
assert_equal 'http://example.org/SWtBvQ', last_response.headers['location']
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_adds_split_url
|
80
|
+
post '/', :url => "https://abc.com\nhttp://abc.com"
|
81
|
+
assert_equal 'http://example.org/cb5CNA', last_response.headers['location']
|
82
|
+
|
83
|
+
assert_equal 'https://abc.comhttp//abc.com', ADAPTER.find('cb5CNA')
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_rejects_non_http_urls
|
87
|
+
post '/', :url => 'ftp://abc.com'
|
88
|
+
assert_equal 422, last_response.status
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_reject_shortened_url_from_other_domain
|
92
|
+
App.set :service, Service.new(ADAPTER, 'abc.com')
|
93
|
+
post '/', :url => 'http://github.com'
|
94
|
+
assert_equal 422, last_response.status
|
95
|
+
assert_match /must be from abc\.com/, last_response.body
|
96
|
+
|
97
|
+
post '/', :url => 'http://abc.com/def'
|
98
|
+
assert_equal 201, last_response.status
|
99
|
+
ensure
|
100
|
+
Guillotine::App.set :required_host, nil
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_reject_shortened_url_from_other_domain_by_regex
|
104
|
+
App.set :service, Service.new(ADAPTER, /abc\.com$/)
|
105
|
+
post '/', :url => 'http://github.com'
|
106
|
+
assert_equal 422, last_response.status
|
107
|
+
assert_match /must match \/abc\\.com/, last_response.body
|
108
|
+
|
109
|
+
post '/', :url => 'http://abc.com/def'
|
110
|
+
assert_equal 201, last_response.status
|
111
|
+
|
112
|
+
post '/', :url => 'http://www.abc.com/def'
|
113
|
+
assert_equal 201, last_response.status
|
114
|
+
ensure
|
115
|
+
App.set :service, SERVICE
|
116
|
+
end
|
117
|
+
|
118
|
+
def app
|
119
|
+
App
|
120
|
+
end
|
118
121
|
end
|
119
122
|
end
|
123
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'mongo'
|
5
|
+
|
6
|
+
class MongoAdapterTest < Guillotine::TestCase
|
7
|
+
def setup
|
8
|
+
@collection = Mongo::Connection.new.db('test').collection('guillotine')
|
9
|
+
@collection.drop
|
10
|
+
@db = Guillotine::Adapters::MongoAdapter.new(@collection)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_adding_a_link_returns_code
|
14
|
+
code = @db.add 'abc'
|
15
|
+
assert_equal 'abc', @db.find(code)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_adding_duplicate_link_returns_same_code
|
19
|
+
code = @db.add 'abc'
|
20
|
+
assert_equal code, @db.add('abc')
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_adds_url_with_custom_code
|
24
|
+
assert_equal 'code', @db.add('def', 'code')
|
25
|
+
assert_equal 'def', @db.find('code')
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_clashing_urls_raises_error
|
29
|
+
code = @db.add 'abc'
|
30
|
+
assert_raises Guillotine::DuplicateCodeError do
|
31
|
+
@db.add 'def', code
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_missing_code
|
36
|
+
assert_nil @db.find('missing')
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_gets_code_for_url
|
40
|
+
code = @db.add 'abc'
|
41
|
+
assert_equal code, @db.code_for('abc')
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_clears_code_for_url
|
45
|
+
code = @db.add 'abc'
|
46
|
+
assert_equal 'abc', @db.find(code)
|
47
|
+
|
48
|
+
@db.clear 'abc'
|
49
|
+
|
50
|
+
assert_nil @db.find(code)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
rescue LoadError
|
54
|
+
puts "Skipping MongoDB tests: #{$!}"
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "redis"
|
5
|
+
|
6
|
+
class RedisAdapterTest < Guillotine::TestCase
|
7
|
+
redis = Redis.new
|
8
|
+
ADAPTER = Guillotine::Adapters::RedisAdapter.new redis
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@db = ADAPTER
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_adding_a_link_returns_code
|
15
|
+
code = @db.add 'abc'
|
16
|
+
assert_equal 'abc', @db.find(code)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_adding_duplicate_link_returns_same_code
|
20
|
+
code = @db.add 'abc'
|
21
|
+
assert_equal code, @db.add('abc')
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_adds_url_with_custom_code
|
25
|
+
assert_equal 'code', @db.add('def', 'code')
|
26
|
+
assert_equal 'def', @db.find('code')
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_clashing_urls_raises_error
|
30
|
+
code = @db.add 'abc'
|
31
|
+
assert_raise Guillotine::DuplicateCodeError do
|
32
|
+
@db.add 'ghi', code
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_missing_code
|
37
|
+
assert_nil @db.find('missing')
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_gets_code_for_url
|
41
|
+
code = @db.add 'abc'
|
42
|
+
assert_equal code, @db.code_for('abc')
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_clears_code_for_url
|
46
|
+
code = @db.add 'abc'
|
47
|
+
assert_equal 'abc', @db.find(code)
|
48
|
+
|
49
|
+
@db.clear 'abc'
|
50
|
+
|
51
|
+
assert_nil @db.find(code)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
rescue LoadError
|
55
|
+
puts "Skipping Redis tests: #{$!}"
|
56
|
+
end
|
data/test/riak_adapter_test.rb
CHANGED
@@ -2,6 +2,12 @@ require File.expand_path('../helper', __FILE__)
|
|
2
2
|
|
3
3
|
begin
|
4
4
|
require 'riak/client'
|
5
|
+
# try Riak::Client with the default Riak port
|
6
|
+
RIAK_TEST_CLIENT = Riak::Client.new
|
7
|
+
# now try it with the default Riak dev port
|
8
|
+
RIAK_TEST_CLIENT.http_port = 8091 unless RIAK_TEST_CLIENT.ping
|
9
|
+
|
10
|
+
raise LoadError, "Could not ping (#{RIAK_TEST_CLIENT.inspect})" unless RIAK_TEST_CLIENT.ping
|
5
11
|
|
6
12
|
# Assumes a local dev install of riak is setup
|
7
13
|
#
|
@@ -11,9 +17,8 @@ begin
|
|
11
17
|
#
|
12
18
|
# http://localhost:8091/riak/guillotine-test
|
13
19
|
class RiakAdapterTest < Guillotine::TestCase
|
14
|
-
|
15
|
-
|
16
|
-
URL_BUCKET = client["guillotine-url-test-#{Process.pid}"]
|
20
|
+
CODE_BUCKET = RIAK_TEST_CLIENT["guillotine-code-test-#{Process.pid}"]
|
21
|
+
URL_BUCKET = RIAK_TEST_CLIENT["guillotine-url-test-#{Process.pid}"]
|
17
22
|
ADAPTER = Guillotine::Adapters::RiakAdapter.new CODE_BUCKET, URL_BUCKET
|
18
23
|
|
19
24
|
def setup
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
module Guillotine
|
4
|
+
class ServiceTest < TestCase
|
5
|
+
def setup
|
6
|
+
@db = Adapters::MemoryAdapter.new
|
7
|
+
@service = Service.new @db
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_adding_a_link_returns_code
|
11
|
+
url = 'http://github.com'
|
12
|
+
status, head, body = @service.create(url + '?a=1')
|
13
|
+
assert_equal 201, status
|
14
|
+
assert code_url = head['Location']
|
15
|
+
code = code_url.gsub(/.*\//, '')
|
16
|
+
|
17
|
+
status, head, body = @service.get(code)
|
18
|
+
assert_equal 302, status
|
19
|
+
assert_equal url, head['Location']
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_adding_duplicate_link_returns_same_code
|
23
|
+
url = 'http://github.com'
|
24
|
+
code = @db.add url
|
25
|
+
|
26
|
+
status, head, body = @service.create(url + '#a=1')
|
27
|
+
assert code_url = head['Location']
|
28
|
+
assert_equal code, code_url.gsub(/.*\//, '')
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_adds_url_with_custom_code
|
32
|
+
url = 'http://github.com/abc'
|
33
|
+
|
34
|
+
status, head, body = @service.create(url, 'code')
|
35
|
+
assert code_url = head['Location']
|
36
|
+
assert_equal 'code', code_url
|
37
|
+
|
38
|
+
status, head, body = @service.get('code')
|
39
|
+
assert_equal 302, status
|
40
|
+
assert_equal url, head['Location']
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_adds_url_with_custom_unicode
|
44
|
+
url = 'http://github.com/abc'
|
45
|
+
status, head, body = @service.create(url, '%E2%9C%88')
|
46
|
+
assert code_url = head['Location']
|
47
|
+
assert_match /^%E2%9C%88$/, code_url
|
48
|
+
|
49
|
+
status, head, body = @service.get("%E2%9C%88")
|
50
|
+
assert_equal 302, status
|
51
|
+
assert_equal url, head['Location']
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_redirects_to_split_url
|
55
|
+
url = "http://abc.com\nhttp//def.com"
|
56
|
+
@db.hash['split'] = url
|
57
|
+
@db.urls[url] = 'split'
|
58
|
+
|
59
|
+
status, head, body = @service.get('split')
|
60
|
+
assert_equal "http://abc.comhttp//def.com", head['Location']
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_clashing_urls_raises_error
|
64
|
+
code = @db.add 'http://github.com/123'
|
65
|
+
status, head, body = @service.create('http://github.com/456', code)
|
66
|
+
assert_equal 422, status
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_add_without_url
|
70
|
+
status, head, body = @service.create(nil)
|
71
|
+
assert_equal 422, status
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_add_url_with_linebreak
|
75
|
+
status, head, body = @service.create("https://abc.com\n")
|
76
|
+
assert_equal 'SWtBvQ', head['Location']
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_adds_split_url
|
80
|
+
status, head, body = @service.create("https://abc.com\nhttp://abc.com")
|
81
|
+
assert_equal 'cb5CNA', head['Location']
|
82
|
+
|
83
|
+
assert_equal 'https://abc.comhttp//abc.com', @db.find('cb5CNA')
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_rejects_non_http_urls
|
87
|
+
status, head, body = @service.create('ftp://abc.com')
|
88
|
+
assert_equal 422, status
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_reject_shortened_url_from_other_domain
|
92
|
+
service = Service.new @db, 'abc.com'
|
93
|
+
status, head, body = service.create('http://github.com')
|
94
|
+
assert_equal 422, status
|
95
|
+
assert_match /must be from abc\.com/, body
|
96
|
+
|
97
|
+
status, head, body = service.create('http://abc.com/def')
|
98
|
+
assert_equal 201, status
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_reject_shortened_url_from_other_domain_by_regex
|
102
|
+
service = Service.new @db, /abc\.com$/
|
103
|
+
status, head, body = service.create('http://github.com')
|
104
|
+
assert_equal 422, status
|
105
|
+
assert_match /must match \/abc\\.com/, body
|
106
|
+
|
107
|
+
status, head, body = service.create('http://abc.com/def')
|
108
|
+
assert_equal 201, status
|
109
|
+
|
110
|
+
status, head, body = service.create('http://www.abc.com/def')
|
111
|
+
assert_equal 201, status
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: guillotine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-11-
|
12
|
+
date: 2011-11-26 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sinatra
|
16
|
-
requirement: &
|
16
|
+
requirement: &70212478964620 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 1.2.6
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70212478964620
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: addressable
|
27
|
-
requirement: &
|
27
|
+
requirement: &70212478964160 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 2.2.6
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70212478964160
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rack-test
|
38
|
-
requirement: &
|
38
|
+
requirement: &70212478980140 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,7 +43,7 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70212478980140
|
47
47
|
description: Adaptable private URL shortener
|
48
48
|
email: technoweenie@gmail.com
|
49
49
|
executables: []
|
@@ -59,15 +59,22 @@ files:
|
|
59
59
|
- lib/guillotine.rb
|
60
60
|
- lib/guillotine/adapters/active_record_adapter.rb
|
61
61
|
- lib/guillotine/adapters/memory_adapter.rb
|
62
|
+
- lib/guillotine/adapters/mongo_adapter.rb
|
63
|
+
- lib/guillotine/adapters/redis_adapter.rb
|
62
64
|
- lib/guillotine/adapters/riak_adapter.rb
|
63
65
|
- lib/guillotine/adapters/sequel_adapter.rb
|
64
66
|
- lib/guillotine/app.rb
|
67
|
+
- lib/guillotine/service.rb
|
68
|
+
- script/cibuild
|
65
69
|
- test/active_record_adapter_test.rb
|
66
70
|
- test/app_test.rb
|
67
71
|
- test/helper.rb
|
68
72
|
- test/memory_adapter_test.rb
|
73
|
+
- test/mongo_adapter_test.rb
|
74
|
+
- test/redis_adapter_test.rb
|
69
75
|
- test/riak_adapter_test.rb
|
70
76
|
- test/sequel_adapter_test.rb
|
77
|
+
- test/service_test.rb
|
71
78
|
homepage: https://github.com/technoweenie/gitio
|
72
79
|
licenses: []
|
73
80
|
post_install_message:
|
@@ -80,6 +87,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
87
|
- - ! '>='
|
81
88
|
- !ruby/object:Gem::Version
|
82
89
|
version: '0'
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
hash: 2311963122741215687
|
83
93
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
94
|
none: false
|
85
95
|
requirements:
|
@@ -88,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
98
|
version: '0'
|
89
99
|
requirements: []
|
90
100
|
rubyforge_project: guillotine
|
91
|
-
rubygems_version: 1.8.
|
101
|
+
rubygems_version: 1.8.11
|
92
102
|
signing_key:
|
93
103
|
specification_version: 2
|
94
104
|
summary: Adaptable private URL shortener
|
@@ -96,5 +106,8 @@ test_files:
|
|
96
106
|
- test/active_record_adapter_test.rb
|
97
107
|
- test/app_test.rb
|
98
108
|
- test/memory_adapter_test.rb
|
109
|
+
- test/mongo_adapter_test.rb
|
110
|
+
- test/redis_adapter_test.rb
|
99
111
|
- test/riak_adapter_test.rb
|
100
112
|
- test/sequel_adapter_test.rb
|
113
|
+
- test/service_test.rb
|