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 CHANGED
@@ -1,12 +1,22 @@
1
1
  source 'http://rubygems.org'
2
2
 
3
- gem 'sinatra'
4
- gem 'addressable'
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.8'
17
- s.date = '2011-11-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
- if row = Url.select(:url).where(:code => code).first
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
- if row = Url.select(:code).where(:url => url).first
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).delete
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
- if row = @table.select(:url).where(:code => code).first
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
- if row = @table.select(:code).where(:url => url).first
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
- string :url
68
- string :code
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
@@ -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 :required_host, nil
7
+ set :service, nil
6
8
 
7
9
  get "/:code" do
8
- code = params[:code]
9
- if url = settings.db.find(Addressable::URI.escape(code))
10
- redirect settings.db.parse_url(url).to_s
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
- url = settings.db.parse_url params[:url].to_s
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
- case settings.required_host
24
- when String
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
- begin
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.8"
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
@@ -0,0 +1,2 @@
1
+ bundle install --binstubs --path vendor/gems
2
+ bin/rake
@@ -11,24 +11,24 @@ begin
11
11
  end
12
12
 
13
13
  def test_adding_a_link_returns_code
14
- code = @db.add 'abc'
15
- assert_equal 'abc', @db.find(code)
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 'abc'
20
- assert_equal code, @db.add('abc')
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('def', 'code')
25
- assert_equal 'def', @db.find('code')
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 '123'
29
+ code = @db.add 'ddd'
30
30
  assert_raises Guillotine::DuplicateCodeError do
31
- code = @db.add '456', code
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 'abc'
41
- assert_equal code, @db.code_for('abc')
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 'abc'
46
- assert_equal 'abc', @db.find(code)
45
+ code = @db.add 'ggg'
46
+ assert_equal 'ggg', @db.find(code)
47
47
 
48
- @db.clear 'abc'
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
- class AppTest < Guillotine::TestCase
4
- ADAPTER = Guillotine::Adapters::MemoryAdapter.new
5
- Guillotine::App.set :db, ADAPTER
6
-
7
- include Rack::Test::Methods
8
-
9
- def test_adding_a_link_returns_code
10
- url = 'http://github.com'
11
- post '/', :url => url + '?a=1'
12
- assert_equal 201, last_response.status
13
- assert code_url = last_response.headers['Location']
14
- code = code_url.gsub(/.*\//, '')
15
-
16
- get "/#{code}"
17
- assert_equal 302, last_response.status
18
- assert_equal url, last_response.headers['Location']
19
- end
20
-
21
- def test_adding_duplicate_link_returns_same_code
22
- url = 'http://github.com'
23
- code = ADAPTER.add url
24
-
25
- post '/', :url => url + '#a=1'
26
- assert code_url = last_response.headers['Location']
27
- assert_equal code, code_url.gsub(/.*\//, '')
28
- end
29
-
30
- def test_adds_url_with_custom_code
31
- url = 'http://github.com/abc'
32
- post '/', :url => url, :code => 'code'
33
- assert code_url = last_response.headers['Location']
34
- assert_match /\/code$/, code_url
35
-
36
- get "/code"
37
- assert_equal 302, last_response.status
38
- assert_equal url, last_response.headers['Location']
39
- end
40
-
41
- def test_adds_url_with_custom_code
42
- url = 'http://github.com/abc'
43
- post '/', :url => url, :code => '%E2%9C%88'
44
- assert code_url = last_response.headers['Location']
45
- assert_match /\/%E2%9C%88$/, code_url
46
-
47
- get "/%E2%9C%88"
48
- assert_equal 302, last_response.status
49
- assert_equal url, last_response.headers['Location']
50
- end
51
-
52
- def test_redirects_to_split_url
53
- url = "http://abc.com\nhttp//def.com"
54
- ADAPTER.hash['split'] = url
55
- ADAPTER.urls[url] = 'split'
56
-
57
- get '/split'
58
- assert_equal "http://abc.comhttp//def.com", last_response.headers['location']
59
- end
60
-
61
- def test_clashing_urls_raises_error
62
- code = ADAPTER.add 'http://github.com/123'
63
- post '/', :url => 'http://github.com/456', :code => code
64
- assert_equal 422, last_response.status
65
- end
66
-
67
- def test_add_without_url
68
- post '/'
69
- assert_equal 422, last_response.status
70
- end
71
-
72
- def test_add_url_with_linebreak
73
- post '/', :url => "https://abc.com\n"
74
- assert_equal 'http://example.org/SWtBvQ', last_response.headers['location']
75
- end
76
-
77
- def test_adds_split_url
78
- post '/', :url => "https://abc.com\nhttp://abc.com"
79
- assert_equal 'http://example.org/cb5CNA', last_response.headers['location']
80
-
81
- assert_equal 'https://abc.comhttp//abc.com', ADAPTER.find('cb5CNA')
82
- end
83
-
84
- def test_rejects_non_http_urls
85
- post '/', :url => 'ftp://abc.com'
86
- assert_equal 422, last_response.status
87
- end
88
-
89
- def test_reject_shortened_url_from_other_domain
90
- Guillotine::App.set :required_host, 'abc.com'
91
- post '/', :url => 'http://github.com'
92
- assert_equal 422, last_response.status
93
- assert_match /must be from abc\.com/, last_response.body
94
-
95
- post '/', :url => 'http://abc.com/def'
96
- assert_equal 201, last_response.status
97
- ensure
98
- Guillotine::App.set :required_host, nil
99
- end
100
-
101
- def test_reject_shortened_url_from_other_domain_by_regex
102
- Guillotine::App.set :required_host, /abc\.com$/
103
- post '/', :url => 'http://github.com'
104
- assert_equal 422, last_response.status
105
- assert_match /must match \/abc\\.com/, last_response.body
106
-
107
- post '/', :url => 'http://abc.com/def'
108
- assert_equal 201, last_response.status
109
-
110
- post '/', :url => 'http://www.abc.com/def'
111
- assert_equal 201, last_response.status
112
- ensure
113
- Guillotine::App.set :required_host, nil
114
- end
115
-
116
- def app
117
- Guillotine::App
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
@@ -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
- client = Riak::Client.new(:http_port => 8091)
15
- CODE_BUCKET = client["guillotine-code-test-#{Process.pid}"]
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.8
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-11 00:00:00.000000000Z
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: &70215141118220 !ruby/object:Gem::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: *70215141118220
24
+ version_requirements: *70212478964620
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: addressable
27
- requirement: &70215141117700 !ruby/object:Gem::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: *70215141117700
35
+ version_requirements: *70212478964160
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rack-test
38
- requirement: &70215141117280 !ruby/object:Gem::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: *70215141117280
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.10
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