diff --git a/source/.rspec b/source/.rspec
new file mode 100644
index 0000000..83e16f8
--- /dev/null
+++ b/source/.rspec
@@ -0,0 +1,2 @@
+--color
+--require spec_helper
diff --git a/source/Gemfile b/source/Gemfile
index 9627b8b..c511dfa 100644
--- a/source/Gemfile
+++ b/source/Gemfile
@@ -29,6 +29,8 @@ gem 'spring', group: :development
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'
+gem 'httparty'
+
# Use unicorn as the app server
# gem 'unicorn'
@@ -38,4 +40,4 @@ gem 'spring', group: :development
# Use debugger
# gem 'debugger', group: [:development, :test]
gem 'rspec-rails', group: [:development, :test]
-
+ gem 'pry'
diff --git a/source/Gemfile.lock b/source/Gemfile.lock
index fcf8b98..74399e9 100644
--- a/source/Gemfile.lock
+++ b/source/Gemfile.lock
@@ -28,33 +28,43 @@ GEM
thread_safe (~> 0.1)
tzinfo (~> 1.1)
arel (5.0.1.20140414130214)
- builder (3.2.2)
+ builder (3.2.3)
+ coderay (1.1.2)
coffee-rails (4.0.1)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.0)
- coffee-script (2.3.0)
+ coffee-script (2.4.1)
coffee-script-source
execjs
- coffee-script-source (1.8.0)
- diff-lcs (1.2.5)
+ coffee-script-source (1.12.2)
+ concurrent-ruby (1.0.5)
+ diff-lcs (1.3)
erubis (2.7.0)
- execjs (2.2.1)
+ execjs (2.7.0)
hike (1.2.3)
- i18n (0.6.11)
- jbuilder (2.2.2)
- activesupport (>= 3.0.0, < 5)
- multi_json (~> 1.2)
- jquery-rails (3.1.2)
+ httparty (0.15.6)
+ multi_xml (>= 0.5.2)
+ i18n (0.9.5)
+ concurrent-ruby (~> 1.0)
+ jbuilder (2.6.4)
+ activesupport (>= 3.0.0)
+ multi_json (>= 1.2)
+ jquery-rails (3.1.5)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
- json (1.8.1)
- mail (2.6.1)
- mime-types (>= 1.16, < 3)
- mime-types (2.4.1)
- minitest (5.4.2)
- multi_json (1.10.1)
- rack (1.5.2)
- rack-test (0.6.2)
+ json (1.8.6)
+ mail (2.7.0)
+ mini_mime (>= 0.1.1)
+ method_source (0.9.0)
+ mini_mime (1.0.0)
+ minitest (5.11.3)
+ multi_json (1.13.1)
+ multi_xml (0.6.0)
+ pry (0.11.3)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ rack (1.5.5)
+ rack-test (0.6.3)
rack (>= 1.0)
rails (4.1.6)
actionmailer (= 4.1.6)
@@ -71,63 +81,65 @@ GEM
activesupport (= 4.1.6)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
- rake (10.3.2)
- rdoc (4.1.2)
- json (~> 1.4)
- rspec-core (3.1.6)
- rspec-support (~> 3.1.0)
- rspec-expectations (3.1.2)
+ rake (12.3.1)
+ rdoc (4.3.0)
+ rspec-core (3.7.1)
+ rspec-support (~> 3.7.0)
+ rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.1.0)
- rspec-mocks (3.1.3)
- rspec-support (~> 3.1.0)
- rspec-rails (3.1.0)
+ rspec-support (~> 3.7.0)
+ rspec-mocks (3.7.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.7.0)
+ rspec-rails (3.7.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
- rspec-core (~> 3.1.0)
- rspec-expectations (~> 3.1.0)
- rspec-mocks (~> 3.1.0)
- rspec-support (~> 3.1.0)
- rspec-support (3.1.2)
+ rspec-core (~> 3.7.0)
+ rspec-expectations (~> 3.7.0)
+ rspec-mocks (~> 3.7.0)
+ rspec-support (~> 3.7.0)
+ rspec-support (3.7.1)
sass (3.2.19)
- sass-rails (4.0.3)
+ sass-rails (4.0.5)
railties (>= 4.0.0, < 5.0)
- sass (~> 3.2.0)
- sprockets (~> 2.8, <= 2.11.0)
+ sass (~> 3.2.2)
+ sprockets (~> 2.8, < 3.0)
sprockets-rails (~> 2.0)
- sdoc (0.4.1)
+ sdoc (0.4.2)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
- spring (1.1.3)
- sprockets (2.11.0)
+ spring (1.7.2)
+ sprockets (2.12.4)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
- sprockets-rails (2.2.0)
+ sprockets-rails (2.3.3)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
- sqlite3 (1.3.9)
- thor (0.19.1)
- thread_safe (0.3.4)
+ sqlite3 (1.3.13)
+ thor (0.20.0)
+ thread_safe (0.3.6)
tilt (1.4.1)
- turbolinks (2.4.0)
- coffee-rails
- tzinfo (1.2.2)
+ turbolinks (5.1.1)
+ turbolinks-source (~> 5.1)
+ turbolinks-source (5.1.0)
+ tzinfo (1.2.5)
thread_safe (~> 0.1)
- uglifier (2.5.3)
- execjs (>= 0.3.0)
- json (>= 1.8.0)
+ uglifier (4.1.10)
+ execjs (>= 0.3.0, < 3)
PLATFORMS
ruby
DEPENDENCIES
coffee-rails (~> 4.0.0)
+ httparty
jbuilder (~> 2.0)
jquery-rails
+ pry
rails (= 4.1.6)
rspec-rails
sass-rails (~> 4.0.3)
@@ -136,3 +148,6 @@ DEPENDENCIES
sqlite3
turbolinks
uglifier (>= 1.3.0)
+
+BUNDLED WITH
+ 1.16.1
diff --git a/source/app/controllers/urls_controller.rb b/source/app/controllers/urls_controller.rb
index ef26710..858bc99 100644
--- a/source/app/controllers/urls_controller.rb
+++ b/source/app/controllers/urls_controller.rb
@@ -1,2 +1,30 @@
+require 'net/http'
+
class UrlsController < ApplicationController
+ def index
+ @urls = Url.all
+ end
+
+ def create
+ url = Url.new(url_params)
+
+ if !url.save
+ flash[:error] = 'Invalid URL'
+ end
+
+ redirect_to urls_path
+ end
+
+ def show
+ url = Url.find(params[:id])
+ url.click_count += 1
+ url.save
+ redirect_to url.long_url
+ end
+
+ private
+
+ def url_params
+ params.require(:url).permit(:long_url)
+ end
end
diff --git a/source/app/models/url.rb b/source/app/models/url.rb
new file mode 100644
index 0000000..eeb42e5
--- /dev/null
+++ b/source/app/models/url.rb
@@ -0,0 +1,24 @@
+class Url < ActiveRecord::Base
+ validates :long_url, presence: true
+ validate :long_url_is_uri_valid
+ validate :long_url_is_reachable
+
+ private
+ def long_url_is_uri_valid
+ unless long_url =~ /\A#{URI::regexp(['http', 'https'])}\z/
+ errors.add(:long_url, 'must be valid uri')
+ end
+ end
+
+ def long_url_is_reachable
+ begin
+ response = HTTParty.get(long_url, timeout: 1)
+
+ if response.code.to_i != 200
+ errors.add(:long_url, 'must return a 200 code')
+ end
+ rescue
+ errors.add(:long_url, 'must be accessible')
+ end
+ end
+end
diff --git a/source/app/views/urls/index.html.erb b/source/app/views/urls/index.html.erb
new file mode 100644
index 0000000..6ab69fa
--- /dev/null
+++ b/source/app/views/urls/index.html.erb
@@ -0,0 +1,14 @@
+
Shorten URL:
+
+<%= flash[:error] if !nil? %>
+
+<%= form_for :url do |f| %>
+ <%= f.label :long_url %>
+ <%= f.text_field :long_url %>
+<% end %>
+
+
+<% @urls.each do |url| %>
+ - <%= url_url(url) ====> #{url.long_url} (clicked #{url.click_count} times)" %>
+<% end %>
+
diff --git a/source/config/routes.rb b/source/config/routes.rb
index 3f66539..8b1aada 100644
--- a/source/config/routes.rb
+++ b/source/config/routes.rb
@@ -53,4 +53,6 @@
# # (app/controllers/admin/products_controller.rb)
# resources :products
# end
+
+ resources :urls, only: [:index, :create, :show]
end
diff --git a/source/db/migrate/20180514142357_create_urls.rb b/source/db/migrate/20180514142357_create_urls.rb
new file mode 100644
index 0000000..604bf55
--- /dev/null
+++ b/source/db/migrate/20180514142357_create_urls.rb
@@ -0,0 +1,9 @@
+class CreateUrls < ActiveRecord::Migration
+ def change
+ create_table :urls do |t|
+ t.string :long_url
+
+ t.timestamps
+ end
+ end
+end
diff --git a/source/db/migrate/20180514175827_add_click_counter_to_urls.rb b/source/db/migrate/20180514175827_add_click_counter_to_urls.rb
new file mode 100644
index 0000000..47e4d97
--- /dev/null
+++ b/source/db/migrate/20180514175827_add_click_counter_to_urls.rb
@@ -0,0 +1,5 @@
+class AddClickCounterToUrls < ActiveRecord::Migration
+ def change
+ add_column :urls, :click_count, :integer
+ end
+end
diff --git a/source/db/migrate/20180516183211_add_default_value_to_click_count_for_urls.rb b/source/db/migrate/20180516183211_add_default_value_to_click_count_for_urls.rb
new file mode 100644
index 0000000..7784cdd
--- /dev/null
+++ b/source/db/migrate/20180516183211_add_default_value_to_click_count_for_urls.rb
@@ -0,0 +1,5 @@
+class AddDefaultValueToClickCountForUrls < ActiveRecord::Migration
+ def change
+ change_column_default :urls, :click_count, 0
+ end
+end
diff --git a/source/db/schema.rb b/source/db/schema.rb
new file mode 100644
index 0000000..f68b2e4
--- /dev/null
+++ b/source/db/schema.rb
@@ -0,0 +1,32 @@
+# encoding: UTF-8
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your
+# database schema. If you need to create the application database on another
+# system, you should be using db:schema:load, not running all the migrations
+# from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 20180516183211) do
+
+ create_table "urls", force: true do |t|
+ t.string "long_url"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "click_count", default: 0
+ t.string "short_url"
+ end
+
+ create_table "users", force: true do |t|
+ t.string "username"
+ t.string "password"
+ t.boolean "admin"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+end
diff --git a/source/spec/controllers/url_controller_spec.rb b/source/spec/controllers/url_controller_spec.rb
new file mode 100644
index 0000000..4f8b509
--- /dev/null
+++ b/source/spec/controllers/url_controller_spec.rb
@@ -0,0 +1,91 @@
+require 'rails_helper'
+
+describe UrlsController do
+ let(:url) { Url.create(long_url: 'https://www.google.com/', click_count: 0) }
+
+ describe 'GET index' do
+ it 'responds with a 200 OK' do
+ get :index
+ expect(response.status).to eq 200
+ end
+
+ it 'assigns @urls' do
+ get :index
+ expect(assigns[:urls]).to eq [url]
+ end
+
+ it 'renders the index template' do
+ get :index
+ expect(response).to render_template('index')
+ end
+ end
+
+ describe 'POST create' do
+ context 'with valid params' do
+ it 'creates a url entry' do
+ expect do
+ post :create, url: { long_url: 'https://www.google.com/' }
+ end.to change { Url.count }.by 1
+ end
+
+ it 'redirects to the index page' do
+ post :create, url: { long_url: 'https://www.google.com/' }
+ expect(response).to redirect_to urls_path
+ end
+
+ it 'sets the click counter to 0' do
+ post :create, url: { long_url: 'https://www.google.com/' }
+ expect(Url.first.click_count).to eq 0
+ end
+ end
+
+ context 'with blantantly invalid params' do
+ it 'does not create a url entry' do
+ expect do
+ post :create, url: { long_url: 'invalid URL' }
+ end.to_not change { Url.count }
+ end
+
+ it 'redirects to the index page' do
+ post :create, url: { long_url: 'invalid URL' }
+ expect(response).to redirect_to urls_path
+ end
+
+ it 'displays an error message' do
+ post :create, url: { long_url: 'invalid URL' }
+ expect(flash[:error]).to eq 'Invalid URL'
+ end
+ end
+
+ context 'with valid params but unaccessible webpage' do
+ it 'does not create a url entry' do
+ expect do
+ post :create, url: { long_url: 'https://blah.com' }
+ end.to_not change { Url.count }
+ end
+
+ it 'redirects to the index page' do
+ post :create, url: { long_url: 'https://blah.com' }
+ expect(response).to redirect_to urls_path
+ end
+
+ it 'displays an error message' do
+ post :create, url: { long_url: 'https://blah.com' }
+ expect(flash[:error]).to eq 'Invalid URL'
+ end
+ end
+ end
+
+ describe 'GET show' do
+ it 'redirects to the long url' do
+ get :show, { id: url.id }
+ expect(response).to redirect_to 'https://www.google.com/'
+ end
+
+ it 'increases the click_counter' do
+ expect do
+ get :show, { id: url.id }
+ end.to change { url.reload.click_count }.by 1
+ end
+ end
+end
diff --git a/source/spec/models/url_spec.rb b/source/spec/models/url_spec.rb
new file mode 100644
index 0000000..38409b9
--- /dev/null
+++ b/source/spec/models/url_spec.rb
@@ -0,0 +1,33 @@
+require 'rails_helper'
+
+RSpec.describe Url, :type => :model do
+ let(:url) { Url.new(long_url: 'https://www.google.com/') }
+
+ describe 'long_url' do
+ it 'is not an empty string' do
+ url.long_url = ''
+ expect(url).to_not be_valid
+ end
+
+ it 'has a prefix http:// or https://' do
+ url.long_url = 'www.google.com'
+ expect(url).to_not be_valid
+
+ url.long_url = 'http://www.google.com'
+ expect(url).to be_valid
+
+ url.long_url = 'https://www.google.com/'
+ expect(url).to be_valid
+ end
+
+ it 'is a valid uri' do
+ url.long_url = 'invalid uri'
+ expect(url).to_not be_valid
+ end
+
+ it 'is accessible' do
+ url.long_url = 'https://invalid.com'
+ expect(url).to_not be_valid
+ end
+ end
+end
diff --git a/source/spec/rails_helper.rb b/source/spec/rails_helper.rb
new file mode 100644
index 0000000..e6c0b68
--- /dev/null
+++ b/source/spec/rails_helper.rb
@@ -0,0 +1,50 @@
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+ENV["RAILS_ENV"] ||= 'test'
+require 'spec_helper'
+require File.expand_path("../../config/environment", __FILE__)
+require 'rspec/rails'
+# Add additional requires below this line. Rails is not loaded until this point!
+
+# Requires supporting ruby files with custom matchers and macros, etc, in
+# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
+# run as spec files by default. This means that files in spec/support that end
+# in _spec.rb will both be required and run as specs, causing the specs to be
+# run twice. It is recommended that you do not name files matching this glob to
+# end with _spec.rb. You can configure this pattern with the --pattern
+# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
+#
+# The following line is provided for convenience purposes. It has the downside
+# of increasing the boot-up time by auto-requiring all files in the support
+# directory. Alternatively, in the individual `*_spec.rb` files, manually
+# require only the support files necessary.
+#
+# Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
+
+# Checks for pending migrations before tests are run.
+# If you are not using ActiveRecord, you can remove this line.
+ActiveRecord::Migration.maintain_test_schema!
+
+RSpec.configure do |config|
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
+
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # examples within a transaction, remove the following line or assign false
+ # instead of true.
+ config.use_transactional_fixtures = true
+
+ # RSpec Rails can automatically mix in different behaviours to your tests
+ # based on their file location, for example enabling you to call `get` and
+ # `post` in specs under `spec/controllers`.
+ #
+ # You can disable this behaviour by removing the line below, and instead
+ # explicitly tag your specs with their type, e.g.:
+ #
+ # RSpec.describe UsersController, :type => :controller do
+ # # ...
+ # end
+ #
+ # The different available types are documented in the features, such as in
+ # https://relishapp.com/rspec/rspec-rails/docs
+ config.infer_spec_type_from_file_location!
+end
diff --git a/source/spec/spec_helper.rb b/source/spec/spec_helper.rb
new file mode 100644
index 0000000..275ba49
--- /dev/null
+++ b/source/spec/spec_helper.rb
@@ -0,0 +1,85 @@
+# This file was generated by the `rails generate rspec:install` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause this
+# file to always be loaded, without a need to explicitly require it in any files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need it.
+#
+# The `.rspec` file also contains a few flags that are not defaults but that
+# users commonly want.
+#
+# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ # rspec-expectations config goes here. You can use an alternate
+ # assertion/expectation library such as wrong or the stdlib/minitest
+ # assertions if you prefer.
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`, e.g.:
+ # be_bigger_than(2).and_smaller_than(4).description
+ # # => "be bigger than 2 and smaller than 4"
+ # ...rather than:
+ # # => "be bigger than 2"
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ # rspec-mocks config goes here. You can use an alternate test double
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+# The settings below are suggested to provide a good initial experience
+# with RSpec, but feel free to customize to your heart's content.
+=begin
+ # These two settings work together to allow you to limit a spec run
+ # to individual examples or groups you care about by tagging them with
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
+ # get run.
+ config.filter_run :focus
+ config.run_all_when_everything_filtered = true
+
+ # Limits the available syntax to the non-monkey patched syntax that is recommended.
+ # For more details, see:
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
+ config.disable_monkey_patching!
+
+ # Many RSpec users commonly either run the entire suite or an individual
+ # file, and it's useful to allow more verbose output when running an
+ # individual spec file.
+ if config.files_to_run.one?
+ # Use the documentation formatter for detailed output,
+ # unless a formatter has already been configured
+ # (e.g. via a command-line flag).
+ config.default_formatter = 'doc'
+ end
+
+ # Print the 10 slowest examples and example groups at the
+ # end of the spec run, to help surface which specs are running
+ # particularly slow.
+ config.profile_examples = 10
+
+ # Run specs in random order to surface order dependencies. If you find an
+ # order dependency and want to debug it, you can fix the order by providing
+ # the seed, which is printed after each run.
+ # --seed 1234
+ config.order = :random
+
+ # Seed global randomization in this process using the `--seed` CLI option.
+ # Setting this allows you to use `--seed` to deterministically reproduce
+ # test failures related to randomization by passing the same `--seed` value
+ # as the one that triggered the failure.
+ Kernel.srand config.seed
+=end
+end