Compare commits

..

9 Commits

10 changed files with 125 additions and 82 deletions

6
.rubocop.yml 100644
View File

@ -0,0 +1,6 @@
AllCops:
NewCops: enable
SuggestExtensions: false
Metrics/BlockLength:
AllowedMethods: ['describe', 'context', 'route', 'r.on', 'r.post']

29
Gemfile
View File

@ -1,32 +1,31 @@
# frozen_string_literal: true # frozen_string_literal: true
source "https://rubygems.org" source 'https://rubygems.org'
gem 'roda', '~> 3.72'
gem "roda", "~> 3.72" gem 'sequel', '~> 5.72'
gem "sequel", "~> 5.72" gem 'tilt', '~> 2.2'
gem "tilt", "~> 2.2" gem 'sqlite3', '~> 1.6'
gem "sqlite3", "~> 1.6" gem 'erubi', '~> 1.12'
gem "erubi", "~> 1.12"
group :test do group :test do
gem 'cucumber', '~> 9.0'
gem "cucumber", "~> 9.0" gem 'capybara', '~> 3.39'
gem "capybara", "~> 3.39" gem 'rspec', '~> 3.12'
gem "rspec", "~> 3.12" gem 'selenium-webdriver', '~> 4.13'
gem "selenium-webdriver", "~> 4.13" gem 'simplecov'
gem "simplecov"
gem 'rubocop', require: false
end end
gem "puma", "~> 6.4" gem 'puma', '~> 6.4'
gem "rackup", "~> 2.1" gem 'rackup', '~> 2.1'

View File

@ -3,6 +3,8 @@ GEM
specs: specs:
addressable (2.8.5) addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
base64 (0.1.1)
bigdecimal (3.1.4) bigdecimal (3.1.4)
builder (3.2.4) builder (3.2.4)
capybara (3.39.2) capybara (3.39.2)
@ -42,6 +44,8 @@ GEM
docile (1.4.0) docile (1.4.0)
erubi (1.12.0) erubi (1.12.0)
ffi (1.16.2) ffi (1.16.2)
json (2.6.3)
language_server-protocol (3.17.0.3)
matrix (0.4.2) matrix (0.4.2)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.4) mini_portile2 (2.8.4)
@ -50,6 +54,10 @@ GEM
nokogiri (1.15.4) nokogiri (1.15.4)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
parallel (1.23.0)
parser (3.2.2.4)
ast (~> 2.4.1)
racc
public_suffix (5.0.3) public_suffix (5.0.3)
puma (6.4.0) puma (6.4.0)
nio4r (~> 2.0) nio4r (~> 2.0)
@ -60,6 +68,7 @@ GEM
rackup (2.1.0) rackup (2.1.0)
rack (>= 3) rack (>= 3)
webrick (~> 1.8) webrick (~> 1.8)
rainbow (3.1.1)
regexp_parser (2.8.1) regexp_parser (2.8.1)
rexml (3.2.6) rexml (3.2.6)
roda (3.72.0) roda (3.72.0)
@ -77,6 +86,21 @@ GEM
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-support (3.12.1) rspec-support (3.12.1)
rubocop (1.57.1)
base64 (~> 0.1.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.2.4)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
ruby-progressbar (1.13.0)
rubyzip (2.3.2) rubyzip (2.3.2)
selenium-webdriver (4.13.1) selenium-webdriver (4.13.1)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
@ -95,6 +119,7 @@ GEM
sys-uname (1.2.3) sys-uname (1.2.3)
ffi (~> 1.1) ffi (~> 1.1)
tilt (2.3.0) tilt (2.3.0)
unicode-display_width (2.5.0)
webrick (1.8.1) webrick (1.8.1)
websocket (1.2.10) websocket (1.2.10)
xpath (3.2.0) xpath (3.2.0)
@ -111,6 +136,7 @@ DEPENDENCIES
rackup (~> 2.1) rackup (~> 2.1)
roda (~> 3.72) roda (~> 3.72)
rspec (~> 3.12) rspec (~> 3.12)
rubocop
selenium-webdriver (~> 4.13) selenium-webdriver (~> 4.13)
sequel (~> 5.72) sequel (~> 5.72)
simplecov simplecov

9
Jenkinsfile vendored
View File

@ -20,6 +20,9 @@ pipeline {
sh 'sequel -m db/migrations sqlite://db/${DB_NAME}' sh 'sequel -m db/migrations sqlite://db/${DB_NAME}'
} }
} }
stage('Code Linting') {
sh 'bundle exec rubocop --format html --out rubocop.html'
}
stage('Run tests') { stage('Run tests') {
steps { steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') { catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
@ -37,9 +40,9 @@ pipeline {
alwaysLinkToLastBuild: false, alwaysLinkToLastBuild: false,
keepAll: true, keepAll: true,
reportDir: '.', reportDir: '.',
reportFiles: 'cucumber.html, spec.html, coverage/index.html', reportFiles: 'rubocop.html, cucumber.html, spec.html, coverage/index.html',
reportName: 'Test Results', reportName: 'Test Results',
reportTitles: 'Cucumber Results, RSpec Results, Test Coverage']) reportTitles: 'Rubocop Results, Cucumber Results, RSpec Results, Test Coverage'])
} }
} }
stage('Build production deliverable') { stage('Build production deliverable') {
@ -53,7 +56,7 @@ pipeline {
cp .env.rb /tmp/url-shortener/ cp .env.rb /tmp/url-shortener/
cp .ruby-version /tmp/url-shortener/ cp .ruby-version /tmp/url-shortener/
cd /tmp/url-shortener cd /tmp/url-shortener
rm -rf features spec coverage db/*.db .git* Jenkinsfile *.html rm -rf features spec coverage db/*.db .git* Jenkinsfile *.html .rubocop.yml
cd /tmp cd /tmp
tar -czvf $ZIP_FILE url-shortener/ tar -czvf $ZIP_FILE url-shortener/
mv /tmp/$ZIP_FILE $CUR_DIR/ mv /tmp/$ZIP_FILE $CUR_DIR/

63
app.rb
View File

@ -1,9 +1,12 @@
# frozen_string_literal: true
require 'roda' require 'roda'
require 'securerandom' require 'securerandom'
require 'json' require 'json'
require 'sequel' require 'sequel'
require 'open-uri' require 'open-uri'
# URL Shortener App class
class App < Roda class App < Roda
plugin :sessions, secret: ENV.delete('APP_SESSION_SECRET') plugin :sessions, secret: ENV.delete('APP_SESSION_SECRET')
plugin :render, escape: true plugin :render, escape: true
@ -11,92 +14,92 @@ class App < Roda
plugin :json_parser plugin :json_parser
plugin :request_headers plugin :request_headers
DB = Sequel.sqlite("db/#{ENV['DB_NAME']}") DB = Sequel.sqlite("db/#{ENV.fetch('DB_NAME', nil)}")
links = DB[:links] links = DB[:links]
route do |r| route do |r|
r.root do r.root do
@message = flash['message'] || "Enter a URL" @message = flash['message'] || 'Enter a URL'
view :home view :home
end end
r.get String do |url_code| r.get String do |url_code|
link = links.filter(:code => url_code) link = links.filter(code: url_code)
r.redirect link.first[:url] unless link.first.nil? r.redirect link.first[:url] unless link.first.nil?
@message = "Link #{url_code} doesn't exist" @message = "Link #{url_code} doesn't exist"
response.status = 404 response.status = 404
view :home view :home
end end
r.post "create" do r.post 'create' do
url = r.params['url'] url = r.params['url']
if url.nil? or url.empty? if url.nil? || url.empty?
flash['message'] = "Please enter a valid URL"; flash['message'] = 'Please enter a valid URL'
r.redirect '/' r.redirect '/'
end end
begin begin
OpenURI.open_uri(url) OpenURI.open_uri(url)
rescue URI::BadURIError rescue URI::BadURIError
flash['message'] = "Invalid URL" flash['message'] = 'Invalid URL'
r.redirect '/' r.redirect '/'
rescue OpenURI::HTTPError rescue OpenURI::HTTPError
flash['message'] = "URL not found" flash['message'] = 'URL not found'
r.redirect '/' r.redirect '/'
rescue SocketError => e rescue SocketError
flash['message'] = "URL does not resolve" flash['message'] = 'URL does not resolve'
r.redirect '/' r.redirect '/'
end end
if links.filter(:url => url).first.nil? if links.filter(url:).first.nil?
code = SecureRandom.urlsafe_base64 4 code = SecureRandom.urlsafe_base64 4
links.insert(url: url, code: code) links.insert(url:, code:)
@message = "Link created" @message = 'Link created'
end end
code = links.filter(:url => url).first[:code] code = links.filter(url:).first[:code]
@message ||= "Link exists" @message ||= 'Link exists'
@new_link = 'http://' + request.env['HTTP_HOST'] + '/' + code @new_link = "http://#{request.env['HTTP_HOST']}/#{code}"
view :create view :create
end end
r.on "links" do r.on 'links' do
r.post do r.post do
if 'application/json' != r.headers['CONTENT_TYPE'] if r.headers['CONTENT_TYPE'] != 'application/json'
response.status = 400 response.status = 400
return {message: "not a valid json request"}.to_json return { message: 'not a valid json request' }.to_json
end end
url = r.params['url'] url = r.params['url']
if url.nil? if url.nil?
response.status = 400 response.status = 400
return {message: "missing url parameter"}.to_json return { message: 'missing url parameter' }.to_json
end end
if url.empty? if url.empty?
response.status = 400 response.status = 400
return {message: "invalid url parameter"}.to_json return { message: 'invalid url parameter' }.to_json
end end
begin begin
OpenURI.open_uri(url) OpenURI.open_uri(url)
rescue URI::BadURIError rescue URI::BadURIError
response.status = 400 response.status = 400
return {message: "invalid url parameter"}.to_json return { message: 'invalid url parameter' }.to_json
rescue OpenURI::HTTPError rescue OpenURI::HTTPError
response.status = 400 response.status = 400
return {message: "url not found"}.to_json return { message: 'url not found' }.to_json
rescue SocketError => e rescue SocketError
response.status = 400 response.status = 400
return {message: "url does not resolve"}.to_json return { message: 'url does not resolve' }.to_json
end end
if links.filter(:url => url).first.nil? if links.filter(url:).first.nil?
code = SecureRandom.urlsafe_base64 4 code = SecureRandom.urlsafe_base64 4
links.insert(url: url, code: code) links.insert(url:, code:)
end end
code = links.filter(:url => url).first[:code] code = links.filter(url:).first[:code]
@new_link = 'http://' + request.env['HTTP_HOST'] + '/' + code @new_link = "http://#{request.env['HTTP_HOST']}/#{code}"
return {url: url, code: code, link: @new_link}.to_json return { url:, code:, link: @new_link }.to_json
end end
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require './.env' require './.env'
require './app' require './app'

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
Sequel.migration do Sequel.migration do
change do change do
create_table :links do create_table :links do

View File

@ -1,9 +1,10 @@
# frozen_string_literal: true
# BEFORE # BEFORE
Before('@db-test') do Before('@db-test') do
@links = Sequel.sqlite("db/#{ENV['DB_NAME']}")[:links] @links = Sequel.sqlite("db/#{ENV.fetch('DB_NAME', nil)}")[:links]
end end
# GIVEN # GIVEN
Given('I visit the {string} page') do |string| Given('I visit the {string} page') do |string|
@ -11,14 +12,13 @@ Given('I visit the {string} page') do |string|
end end
Given('A link already exists with the url {string}') do |string| Given('A link already exists with the url {string}') do |string|
@links.insert(url: string, code: "aaaaaa") @links.insert(url: string, code: 'aaaaaa')
end end
Given('A link already exists with the url {string} and code {string}') do |url, code| Given('A link already exists with the url {string} and code {string}') do |url, code|
@links.insert(url: url, code: code) @links.insert(url:, code:)
end end
# WHEN # WHEN
When('I click the {string} button') do |string| When('I click the {string} button') do |string|
@ -33,7 +33,6 @@ When('I visit the {string} location') do |string|
visit string visit string
end end
# THEN # THEN
Then('I should see text {string}') do |string| Then('I should see text {string}') do |string|
@ -66,7 +65,6 @@ Then('The status code should be {int}') do |code|
page.status_code.should eq(code) page.status_code.should eq(code)
end end
# AFTER # AFTER
After('@db-test') do After('@db-test') do

View File

@ -1,25 +1,27 @@
# frozen_string_literal: true
require 'simplecov' require 'simplecov'
SimpleCov.start SimpleCov.start
require_relative '../../.env' require_relative '../../.env'
ENV["DB_NAME"] = "test_#{ENV["DB_NAME"]}" ENV['DB_NAME'] = "test_#{ENV.fetch('DB_NAME', nil)}"
require_relative '../../app' require_relative '../../app'
require 'rubygems' require 'rubygems'
require 'roda' require 'roda'
require 'sequel' require 'sequel'
require 'capybara' require 'capybara'
require 'capybara/dsl' require 'capybara/dsl'
require 'capybara/cucumber'
require 'rspec' require 'rspec'
###
require 'rspec/expectations'
require 'rspec/matchers'
# DB initialization # DB initialization
Sequel.extension :migration Sequel.extension :migration
Sequel.sqlite("db/#{ENV['DB_NAME']}") do |db| Sequel.sqlite("db/#{ENV.fetch('DB_NAME', nil)}") do |db|
Sequel::Migrator.apply(db, "db/migrations") Sequel::Migrator.apply(db, 'db/migrations')
end end
# attach app to Capybara # attach app to Capybara
Capybara.app = App Capybara.app = App
include Capybara::DSL
include RSpec::Expectations
include RSpec::Matchers

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
require 'simplecov' require 'simplecov'
SimpleCov.start SimpleCov.start
require_relative '../.env' require_relative '../.env'
ENV["DB_NAME"] = "test_#{ENV["DB_NAME"]}" ENV['DB_NAME'] = "test_#{ENV.fetch('DB_NAME', nil)}"
require_relative '../app' require_relative '../app'
require 'rubygems' require 'rubygems'
require 'roda' require 'roda'
@ -12,27 +14,27 @@ require 'rack/test'
# DB initialization # DB initialization
Sequel.extension :migration Sequel.extension :migration
Sequel.sqlite("db/#{ENV['DB_NAME']}") do |db| Sequel.sqlite("db/#{ENV.fetch('DB_NAME', nil)}") do |db|
Sequel::Migrator.apply(db, "db/migrations") Sequel::Migrator.apply(db, 'db/migrations')
end end
def app def app
App App
end end
describe "Submit API request to create new link" do describe 'Submit API request to create new link' do
include Rack::Test::Methods include Rack::Test::Methods
before :each do before :each do
@links = Sequel.sqlite("db/#{ENV['DB_NAME']}")[:links] @links = Sequel.sqlite("db/#{ENV.fetch('DB_NAME', nil)}")[:links]
end end
after :each do after :each do
@links.delete @links.delete
end end
it "should return link data in json format when a valid url is submitted" do it 'should return link data in json format when a valid url is submitted' do
data = { data = {
url: 'http://google.com' url: 'http://google.com'
} }
post('/links', data.to_json, "CONTENT_TYPE" => "application/json") post('/links', data.to_json, 'CONTENT_TYPE' => 'application/json')
expect(last_response).to be_ok expect(last_response).to be_ok
response_json = JSON.parse(last_response.body) response_json = JSON.parse(last_response.body)
expect(response_json['url']).to eq(data[:url]) expect(response_json['url']).to eq(data[:url])
@ -44,16 +46,15 @@ describe "Submit API request to create new link" do
data = { data = {
url: '' url: ''
} }
post('/links', data.to_json, "CONTENT_TYPE" => "application/json") post('/links', data.to_json, 'CONTENT_TYPE' => 'application/json')
expect(last_response.status).to eq(400) expect(last_response.status).to eq(400)
response_json = JSON.parse(last_response.body) response_json = JSON.parse(last_response.body)
expect(response_json['message']).to eq('invalid url parameter') expect(response_json['message']).to eq('invalid url parameter')
end end
it "should return with a 400 status and 'missing url parameter' message when an empty url is submitted" do it "should return with a 400 status and 'missing url parameter' message when an empty url is submitted" do
data = { data = {}
} post('/links', data.to_json, 'CONTENT_TYPE' => 'application/json')
post('/links', data.to_json, "CONTENT_TYPE" => "application/json")
expect(last_response.status).to eq(400) expect(last_response.status).to eq(400)
response_json = JSON.parse(last_response.body) response_json = JSON.parse(last_response.body)
expect(response_json['message']).to eq('missing url parameter') expect(response_json['message']).to eq('missing url parameter')
@ -63,7 +64,7 @@ describe "Submit API request to create new link" do
data = { data = {
url: 'not-an-url' url: 'not-an-url'
} }
post('/links', data.to_json, "CONTENT_TYPE" => "application/json") post('/links', data.to_json, 'CONTENT_TYPE' => 'application/json')
expect(last_response.status).to eq(400) expect(last_response.status).to eq(400)
response_json = JSON.parse(last_response.body) response_json = JSON.parse(last_response.body)
expect(response_json['message']).to eq('invalid url parameter') expect(response_json['message']).to eq('invalid url parameter')
@ -73,7 +74,7 @@ describe "Submit API request to create new link" do
data = { data = {
url: 'http://google.com/example' url: 'http://google.com/example'
} }
post('/links', data.to_json, "CONTENT_TYPE" => "application/json") post('/links', data.to_json, 'CONTENT_TYPE' => 'application/json')
expect(last_response.status).to eq(400) expect(last_response.status).to eq(400)
response_json = JSON.parse(last_response.body) response_json = JSON.parse(last_response.body)
expect(response_json['message']).to eq('url not found') expect(response_json['message']).to eq('url not found')
@ -83,13 +84,14 @@ describe "Submit API request to create new link" do
data = { data = {
url: 'http://bad.tld' url: 'http://bad.tld'
} }
post('/links', data.to_json, "CONTENT_TYPE" => "application/json") post('/links', data.to_json, 'CONTENT_TYPE' => 'application/json')
expect(last_response.status).to eq(400) expect(last_response.status).to eq(400)
response_json = JSON.parse(last_response.body) response_json = JSON.parse(last_response.body)
expect(response_json['message']).to eq('url does not resolve') expect(response_json['message']).to eq('url does not resolve')
end end
it "should return with a 400 status and 'not a valid json request' message when a request is made with the wrong content type header" do it "should return with a 400 status and 'not a valid json request'
message when a request is made with the wrong content type header" do
data = { data = {
url: 'http://google.com' url: 'http://google.com'
} }