guillotine 1.0.8 → 1.1.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.
- 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
|