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 %> + + 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