Compare commits
No commits in common. "master" and "2023-10-15" have entirely different histories.
master
...
2023-10-15
|
|
@ -3,5 +3,3 @@
|
||||||
*.db
|
*.db
|
||||||
.env.rb
|
.env.rb
|
||||||
coverage
|
coverage
|
||||||
vendor
|
|
||||||
.ruby-version
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
AllCops:
|
|
||||||
NewCops: enable
|
|
||||||
SuggestExtensions: false
|
|
||||||
|
|
||||||
Metrics/BlockLength:
|
|
||||||
AllowedMethods: ['describe', 'context', 'route', 'r.on', 'r.post']
|
|
||||||
33
Gemfile
33
Gemfile
|
|
@ -1,35 +1,32 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
source "https://rubygems.org"
|
||||||
|
|
||||||
gem 'rackup', '~> 2.1'
|
|
||||||
|
|
||||||
gem 'falcon', '~> 0.42.3'
|
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 'capybara', '~> 3.39'
|
gem "cucumber", "~> 9.0"
|
||||||
|
|
||||||
gem 'rspec', '~> 3.12'
|
gem "capybara", "~> 3.39"
|
||||||
|
|
||||||
gem 'selenium-webdriver', '~> 4.13'
|
gem "rspec", "~> 3.12"
|
||||||
|
|
||||||
gem 'simplecov'
|
gem "selenium-webdriver", "~> 4.13"
|
||||||
|
|
||||||
gem 'rubocop', require: false
|
gem "simplecov"
|
||||||
|
|
||||||
gem 'ruby_audit', '~> 2.2'
|
|
||||||
|
|
||||||
gem 'bundle-audit', '~> 0.1.0'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
gem "puma", "~> 6.4"
|
||||||
|
|
||||||
|
gem "rackup", "~> 2.1"
|
||||||
|
|
|
||||||
104
Gemfile.lock
104
Gemfile.lock
|
|
@ -3,38 +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)
|
|
||||||
async (2.6.5)
|
|
||||||
console (~> 1.10)
|
|
||||||
fiber-annotation
|
|
||||||
io-event (~> 1.1)
|
|
||||||
timers (~> 4.1)
|
|
||||||
async-container (0.16.12)
|
|
||||||
async
|
|
||||||
async-io
|
|
||||||
async-http (0.61.0)
|
|
||||||
async (>= 1.25)
|
|
||||||
async-io (>= 1.28)
|
|
||||||
async-pool (>= 0.2)
|
|
||||||
protocol-http (~> 0.25.0)
|
|
||||||
protocol-http1 (~> 0.16.0)
|
|
||||||
protocol-http2 (~> 0.15.0)
|
|
||||||
traces (>= 0.10.0)
|
|
||||||
async-http-cache (0.4.3)
|
|
||||||
async-http (~> 0.56)
|
|
||||||
async-io (1.37.0)
|
|
||||||
async
|
|
||||||
async-pool (0.4.0)
|
|
||||||
async (>= 1.25)
|
|
||||||
base64 (0.1.1)
|
|
||||||
bigdecimal (3.1.4)
|
bigdecimal (3.1.4)
|
||||||
build-environment (1.13.0)
|
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bundle-audit (0.1.0)
|
|
||||||
bundler-audit
|
|
||||||
bundler-audit (0.9.1)
|
|
||||||
bundler (>= 1.2.0, < 3)
|
|
||||||
thor (~> 1.0)
|
|
||||||
capybara (3.39.2)
|
capybara (3.39.2)
|
||||||
addressable
|
addressable
|
||||||
matrix
|
matrix
|
||||||
|
|
@ -44,9 +14,6 @@ GEM
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
console (1.23.2)
|
|
||||||
fiber-annotation
|
|
||||||
fiber-local
|
|
||||||
cucumber (9.0.2)
|
cucumber (9.0.2)
|
||||||
builder (~> 3.2, >= 3.2.4)
|
builder (~> 3.2, >= 3.2.4)
|
||||||
cucumber-ci-environment (~> 9.2, >= 9.2.0)
|
cucumber-ci-environment (~> 9.2, >= 9.2.0)
|
||||||
|
|
@ -74,53 +41,18 @@ GEM
|
||||||
diff-lcs (1.5.0)
|
diff-lcs (1.5.0)
|
||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
erubi (1.12.0)
|
erubi (1.12.0)
|
||||||
falcon (0.42.3)
|
|
||||||
async
|
|
||||||
async-container (~> 0.16.0)
|
|
||||||
async-http (~> 0.57)
|
|
||||||
async-http-cache (~> 0.4.0)
|
|
||||||
async-io (~> 1.22)
|
|
||||||
build-environment (~> 1.13)
|
|
||||||
bundler
|
|
||||||
localhost (~> 1.1)
|
|
||||||
openssl (~> 3.0)
|
|
||||||
process-metrics (~> 0.2.0)
|
|
||||||
protocol-rack (~> 0.1)
|
|
||||||
samovar (~> 2.1)
|
|
||||||
ffi (1.16.2)
|
ffi (1.16.2)
|
||||||
fiber-annotation (0.2.0)
|
|
||||||
fiber-local (1.0.0)
|
|
||||||
io-event (1.3.3)
|
|
||||||
json (2.6.3)
|
|
||||||
language_server-protocol (3.17.0.3)
|
|
||||||
localhost (1.1.10)
|
|
||||||
mapping (1.1.1)
|
|
||||||
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)
|
||||||
multi_test (1.1.0)
|
multi_test (1.1.0)
|
||||||
|
nio4r (2.5.9)
|
||||||
nokogiri (1.15.4)
|
nokogiri (1.15.4)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
openssl (3.2.0)
|
|
||||||
parallel (1.23.0)
|
|
||||||
parser (3.2.2.4)
|
|
||||||
ast (~> 2.4.1)
|
|
||||||
racc
|
|
||||||
process-metrics (0.2.1)
|
|
||||||
console (~> 1.8)
|
|
||||||
samovar (~> 2.1)
|
|
||||||
protocol-hpack (1.4.2)
|
|
||||||
protocol-http (0.25.0)
|
|
||||||
protocol-http1 (0.16.0)
|
|
||||||
protocol-http (~> 0.22)
|
|
||||||
protocol-http2 (0.15.1)
|
|
||||||
protocol-hpack (~> 1.4)
|
|
||||||
protocol-http (~> 0.18)
|
|
||||||
protocol-rack (0.2.6)
|
|
||||||
protocol-http (~> 0.23)
|
|
||||||
rack (>= 1.0)
|
|
||||||
public_suffix (5.0.3)
|
public_suffix (5.0.3)
|
||||||
|
puma (6.4.0)
|
||||||
|
nio4r (~> 2.0)
|
||||||
racc (1.7.1)
|
racc (1.7.1)
|
||||||
rack (3.0.8)
|
rack (3.0.8)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
|
|
@ -128,7 +60,6 @@ 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)
|
||||||
|
|
@ -146,27 +77,7 @@ 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)
|
|
||||||
ruby_audit (2.2.0)
|
|
||||||
bundler-audit (~> 0.9.0)
|
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
samovar (2.2.0)
|
|
||||||
console (~> 1.0)
|
|
||||||
mapping (~> 1.0)
|
|
||||||
selenium-webdriver (4.13.1)
|
selenium-webdriver (4.13.1)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
rubyzip (>= 1.2.2, < 3.0)
|
rubyzip (>= 1.2.2, < 3.0)
|
||||||
|
|
@ -183,11 +94,7 @@ GEM
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
sys-uname (1.2.3)
|
sys-uname (1.2.3)
|
||||||
ffi (~> 1.1)
|
ffi (~> 1.1)
|
||||||
thor (1.3.0)
|
|
||||||
tilt (2.3.0)
|
tilt (2.3.0)
|
||||||
timers (4.3.5)
|
|
||||||
traces (0.11.1)
|
|
||||||
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)
|
||||||
|
|
@ -197,16 +104,13 @@ PLATFORMS
|
||||||
x86_64-freebsd-13
|
x86_64-freebsd-13
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
bundle-audit (~> 0.1.0)
|
|
||||||
capybara (~> 3.39)
|
capybara (~> 3.39)
|
||||||
cucumber (~> 9.0)
|
cucumber (~> 9.0)
|
||||||
erubi (~> 1.12)
|
erubi (~> 1.12)
|
||||||
falcon (~> 0.42.3)
|
puma (~> 6.4)
|
||||||
rackup (~> 2.1)
|
rackup (~> 2.1)
|
||||||
roda (~> 3.72)
|
roda (~> 3.72)
|
||||||
rspec (~> 3.12)
|
rspec (~> 3.12)
|
||||||
rubocop
|
|
||||||
ruby_audit (~> 2.2)
|
|
||||||
selenium-webdriver (~> 4.13)
|
selenium-webdriver (~> 4.13)
|
||||||
sequel (~> 5.72)
|
sequel (~> 5.72)
|
||||||
simplecov
|
simplecov
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
def buildArtifact = true
|
|
||||||
|
|
||||||
pipeline {
|
pipeline {
|
||||||
agent { label 'ruby && freebsd' }
|
agent any
|
||||||
|
|
||||||
environment {
|
environment {
|
||||||
|
APP_SESSION_SECRET = ''
|
||||||
DB_NAME = 'url_shortener.db'
|
DB_NAME = 'url_shortener.db'
|
||||||
}
|
}
|
||||||
stages {
|
stages {
|
||||||
stage('Init') {
|
stage('Init') {
|
||||||
steps {
|
steps {
|
||||||
sh ''' #!/usr/local/bin/bash
|
sh 'rbenv local 3.2.2'
|
||||||
rbenv local 3.2.2
|
script {
|
||||||
echo "# frozen_string_literal: true\n" > .env.rb
|
env.APP_SESSION_SECRET = sh(script: 'ruby secret.rb', returnStdout: true)
|
||||||
echo "ENV['APP_SESSION_SECRET'] ||= '$(ruby -rsecurerandom -e 'puts SecureRandom.base64(64)')'" >> .env.rb
|
}
|
||||||
echo "ENV['DB_NAME'] ||= '${DB_NAME}'" >> .env.rb
|
sh 'echo "ENV[\\\"APP_SESSION_SECRET\\\"] ||= $(ruby secret.rb)" > .env.rb'
|
||||||
cat .env.rb
|
sh 'echo "ENV[\\\"DB_NAME\\\"] ||= \\\"${DB_NAME}\\\"" >> .env.rb'
|
||||||
'''
|
sh 'cat .env.rb'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Build dependencies') {
|
stage('Build dependencies') {
|
||||||
|
|
@ -24,131 +23,27 @@ pipeline {
|
||||||
sh 'sequel -m db/migrations sqlite://db/${DB_NAME}'
|
sh 'sequel -m db/migrations sqlite://db/${DB_NAME}'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Audit Dependencies') {
|
|
||||||
steps {
|
|
||||||
catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
|
|
||||||
script {
|
|
||||||
try {
|
|
||||||
sh 'bundle exec ruby-audit check'
|
|
||||||
sh 'bundle exec bundle-audit check >> audit.html'
|
|
||||||
} catch (e) {
|
|
||||||
script {
|
|
||||||
buildArtifact = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Code Linting') {
|
|
||||||
steps {
|
|
||||||
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
|
||||||
script {
|
|
||||||
try {
|
|
||||||
sh 'bundle exec rubocop --format html --out rubocop.html'
|
|
||||||
} catch (e) {
|
|
||||||
script {
|
|
||||||
buildArtifact = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Run tests') {
|
stage('Run tests') {
|
||||||
steps {
|
steps {
|
||||||
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
||||||
script {
|
sh 'cucumber features --format html --out cucumber.html'
|
||||||
try {
|
|
||||||
sh 'bundle exec cucumber features --format html --out cucumber.html'
|
|
||||||
} catch (e) {
|
|
||||||
script {
|
|
||||||
buildArtifact = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
||||||
script {
|
sh 'rspec spec --format html --out spec.html'
|
||||||
try {
|
|
||||||
sh 'bundle exec rspec spec --format html --out spec.html'
|
|
||||||
} catch (e) {
|
|
||||||
script {
|
|
||||||
buildArtifact = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stage('Report results') {
|
stage('Report results') {
|
||||||
steps {
|
steps {
|
||||||
|
archive(includes: 'pkg/*.gem')
|
||||||
publishHTML (target: [
|
publishHTML (target: [
|
||||||
allowMissing: false,
|
allowMissing: false,
|
||||||
alwaysLinkToLastBuild: false,
|
alwaysLinkToLastBuild: false,
|
||||||
keepAll: true,
|
keepAll: true,
|
||||||
reportDir: '.',
|
reportDir: '.',
|
||||||
reportFiles: 'rubocop.html, audit.html, cucumber.html, spec.html, coverage/index.html',
|
reportFiles: 'cucumber.html, spec.html, coverage/index.html',
|
||||||
reportName: 'Test Results',
|
reportName: 'Test Results',
|
||||||
reportTitles: 'Rubocop Results, Bundler Audit Results, Cucumber Results, RSpec Results, Test Coverage'])
|
reportTitles: 'Cucumber Results, RSpec Results, Test Coverage'])
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Build production deliverable') {
|
|
||||||
when {
|
|
||||||
expression {
|
|
||||||
buildArtifact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
|
|
||||||
sh ''' #!/usr/local/bin/bash
|
|
||||||
ZIP_FILE="url-shortener_${BRANCH_NAME}_$(cat VERSION | cut -d"'" -f2).tgz"
|
|
||||||
CUR_DIR=$(pwd)
|
|
||||||
bundle config set --local without 'test'
|
|
||||||
bundle config set --local path "vendor"
|
|
||||||
bundle install
|
|
||||||
mkdir -p /tmp/url-shortener
|
|
||||||
cp -R * /tmp/url-shortener
|
|
||||||
cp .env.rb /tmp/url-shortener/
|
|
||||||
cp .ruby-version /tmp/url-shortener/
|
|
||||||
cd /tmp/url-shortener
|
|
||||||
rm -rf features spec coverage db/*.db .git* Jenkinsfile *.html .rubocop.yml
|
|
||||||
cd /tmp
|
|
||||||
tar -czvf $ZIP_FILE url-shortener/
|
|
||||||
mv /tmp/$ZIP_FILE $CUR_DIR/
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
archiveArtifacts artifacts: '*.tgz'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Generate SHA256 Hash and Upload to Artifact Repository') {
|
|
||||||
environment {
|
|
||||||
ARTIFACTS_KEY = credentials('artifactor-key')
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
sshagent(['artifactor-key']) {
|
|
||||||
sh ''' #!/usr/local/bin/bash
|
|
||||||
UUID=$(uuidgen -r)
|
|
||||||
VERSION=$(cat VERSION | cut -d"'" -f2)
|
|
||||||
BRANCH=${BRANCH_NAME}
|
|
||||||
ZIP_FILE="url-shortener_${BRANCH_NAME}_$(cat VERSION | cut -d"'" -f2).tgz"
|
|
||||||
sha256 $ZIP_FILE >> SHA256.sig
|
|
||||||
ssh artifactor@artifacts mkdir -p projects/url-shortener/$BRANCH/$VERSION/$UUID
|
|
||||||
rsync SHA256.sig artifactor@artifacts:projects/url-shortener/$BRANCH/$VERSION/$UUID/
|
|
||||||
rsync $ZIP_FILE artifactor@artifacts:projects/url-shortener/$BRANCH/$VERSION/$UUID/
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Clean up deliverable') {
|
|
||||||
when {
|
|
||||||
expression {
|
|
||||||
buildArtifact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
sh 'rm -rf /tmp/url-shortener'
|
|
||||||
sh 'rm SHA256.sig'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -162,8 +57,5 @@ pipeline {
|
||||||
failure {
|
failure {
|
||||||
mattermostSend channel: 'git-messages', color: 'danger', message: "[${JOB_NAME}](${JOB_URL}) [#${BUILD_NUMBER}](${BUILD_URL}) ([Gitea](${GIT_URL}))", text: "Build Failed"
|
mattermostSend channel: 'git-messages', color: 'danger', message: "[${JOB_NAME}](${JOB_URL}) [#${BUILD_NUMBER}](${BUILD_URL}) ([Gitea](${GIT_URL}))", text: "Build Failed"
|
||||||
}
|
}
|
||||||
always {
|
|
||||||
cleanWs deleteDirs: true, patterns: [[pattern: '*', type: 'INCLUDE']]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
README.md
20
README.md
|
|
@ -1,11 +1,11 @@
|
||||||
# simple url shortener
|
# simple url shortener
|
||||||
|
|
||||||
this project is a simple solution to build out a URL shortener using
|
this project is a simple solution to build out a URL shortener using
|
||||||
Roda, Puma, Sequel, and SQLite on a FreeBSD Jail
|
Roda, Falcon, Sequel, and SQLite
|
||||||
|
|
||||||
the point of this project is to quickly build something and work on continuous deployment while making small refinements to the functional pieces.
|
the point of this project is to quickly build something and work on continuous deployment while making small refinements to the functional pieces.
|
||||||
|
|
||||||
the outside pieces of software that this project relies on are rbenv, ruby-build, sqlite3, and pkgconf.
|
the outside pieces of software that this project relies on are sqlite3 and pkgconf.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,9 +14,9 @@ first you have to install the dependencies:
|
||||||
`bundle install`
|
`bundle install`
|
||||||
|
|
||||||
|
|
||||||
if you want to skip the test dependencies:
|
if you want the development group included run this first:
|
||||||
|
|
||||||
`bundle config set --local without 'test'`
|
`bundle config set --local with 'development'`
|
||||||
|
|
||||||
|
|
||||||
then create a .env.rb file in the root directory that contains the following ENV attributes:
|
then create a .env.rb file in the root directory that contains the following ENV attributes:
|
||||||
|
|
@ -24,19 +24,15 @@ then create a .env.rb file in the root directory that contains the following ENV
|
||||||
```
|
```
|
||||||
ENV["APP_SESSION_SECRET"] = {output of a random 64 byte secret}
|
ENV["APP_SESSION_SECRET"] = {output of a random 64 byte secret}
|
||||||
|
|
||||||
ENV["DB_NAME"] = {DB_NAME}
|
ENV["DB_NAME"] = {db file name}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
after the dependencies are installed, you have to create the db and schema
|
after the dependencies are installed, you have to create the db
|
||||||
|
|
||||||
`sequel -m db/migrations sqlite://db/{DB_NAME}`
|
`sequel -m db/migrations sqlite://db/{DB_NAME}`
|
||||||
|
|
||||||
|
|
||||||
to start the application with Puma:
|
to start the application with Falcon:
|
||||||
|
|
||||||
`rackup -o {ip address} -p {port}`
|
`rackup -o {ip address} -p {port} -s falcon`
|
||||||
|
|
||||||
to run it as a daemon:
|
|
||||||
|
|
||||||
`rackup -o {ip address} -p {port} -D`
|
|
||||||
|
|
|
||||||
69
app.rb
69
app.rb
|
|
@ -1,12 +1,9 @@
|
||||||
# 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
|
||||||
|
|
@ -14,92 +11,92 @@ class App < Roda
|
||||||
plugin :json_parser
|
plugin :json_parser
|
||||||
plugin :request_headers
|
plugin :request_headers
|
||||||
|
|
||||||
DB = Sequel.sqlite("db/#{ENV.fetch('DB_NAME', nil)}")
|
DB = Sequel.sqlite("db/#{ENV['DB_NAME']}")
|
||||||
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? || url.empty?
|
if url.nil? or 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
|
rescue SocketError => e
|
||||||
flash['message'] = 'URL does not resolve'
|
flash['message'] = "URL does not resolve"
|
||||||
r.redirect '/'
|
r.redirect '/'
|
||||||
end
|
end
|
||||||
|
|
||||||
if links.filter(url:).first.nil?
|
if links.filter(:url => url).first.nil?
|
||||||
code = SecureRandom.urlsafe_base64 4
|
code = SecureRandom.urlsafe_base64 4
|
||||||
links.insert(url:, code:)
|
links.insert(url: url, code: code)
|
||||||
@message = 'Link created'
|
@message = "Link created"
|
||||||
end
|
end
|
||||||
code = links.filter(url:).first[:code]
|
code = links.filter(:url => 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 r.headers['CONTENT_TYPE'] != 'application/json'
|
if 'application/json' != r.headers['CONTENT_TYPE']
|
||||||
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
|
rescue SocketError => e
|
||||||
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:).first.nil?
|
if links.filter(:url => url).first.nil?
|
||||||
code = SecureRandom.urlsafe_base64 4
|
code = SecureRandom.urlsafe_base64 4
|
||||||
links.insert(url:, code:)
|
links.insert(url: url, code: code)
|
||||||
end
|
end
|
||||||
|
|
||||||
code = links.filter(url:).first[:code]
|
code = links.filter(:url => url).first[:code]
|
||||||
@new_link = "http://#{request.env['HTTP_HOST']}/#{code}"
|
@new_link = 'http://' + request.env['HTTP_HOST'] + '/' + code
|
||||||
return { url:, code:, link: @new_link }.to_json
|
return {url: url, code: code, link: @new_link}.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require './.env'
|
require './.env'
|
||||||
require './app'
|
require './app'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
Sequel.migration do
|
Sequel.migration do
|
||||||
change do
|
change do
|
||||||
create_table :links do
|
create_table :links do
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# BEFORE
|
# BEFORE
|
||||||
Before('@db-test') do
|
Before('@db-test') do
|
||||||
@links = Sequel.sqlite("db/#{ENV.fetch('DB_NAME', nil)}")[:links]
|
@links = Sequel.sqlite("db/#{ENV['DB_NAME']}")[:links]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# GIVEN
|
# GIVEN
|
||||||
|
|
||||||
Given('I visit the {string} page') do |string|
|
Given('I visit the {string} page') do |string|
|
||||||
|
|
@ -12,17 +11,18 @@ 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:, code:)
|
@links.insert(url: url, code: code)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# WHEN
|
# WHEN
|
||||||
|
|
||||||
When('I click the {string} button') do |string|
|
When('I click the {string} button') do |string|
|
||||||
click_button { string }
|
click_button {string}
|
||||||
end
|
end
|
||||||
|
|
||||||
When('I type {string} in the {string} field') do |text, field|
|
When('I type {string} in the {string} field') do |text, field|
|
||||||
|
|
@ -33,6 +33,7 @@ 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|
|
||||||
|
|
@ -65,6 +66,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,25 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'simplecov'
|
require 'simplecov'
|
||||||
SimpleCov.start
|
SimpleCov.start
|
||||||
|
|
||||||
require_relative '../../.env'
|
require_relative '../../.env'
|
||||||
ENV['DB_NAME'] = "test_#{ENV.fetch('DB_NAME', nil)}"
|
ENV["DB_NAME"] = "test_#{ENV["DB_NAME"]}"
|
||||||
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.fetch('DB_NAME', nil)}") do |db|
|
Sequel.sqlite("db/#{ENV['DB_NAME']}") 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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
require 'securerandom'
|
||||||
|
puts SecureRandom.base64(64).inspect()
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'simplecov'
|
require 'simplecov'
|
||||||
SimpleCov.start
|
SimpleCov.start
|
||||||
|
|
||||||
require_relative '../.env'
|
require_relative '../.env'
|
||||||
ENV['DB_NAME'] = "test_#{ENV.fetch('DB_NAME', nil)}"
|
ENV["DB_NAME"] = "test_#{ENV["DB_NAME"]}"
|
||||||
require_relative '../app'
|
require_relative '../app'
|
||||||
require 'rubygems'
|
require 'rubygems'
|
||||||
require 'roda'
|
require 'roda'
|
||||||
|
|
@ -14,47 +12,48 @@ require 'rack/test'
|
||||||
|
|
||||||
# DB initialization
|
# DB initialization
|
||||||
Sequel.extension :migration
|
Sequel.extension :migration
|
||||||
Sequel.sqlite("db/#{ENV.fetch('DB_NAME', nil)}") do |db|
|
Sequel.sqlite("db/#{ENV['DB_NAME']}") 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.fetch('DB_NAME', nil)}")[:links]
|
@links = Sequel.sqlite("db/#{ENV['DB_NAME']}")[: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])
|
||||||
expect(response_json['code']).not_to eq(nil)
|
expect(response_json['code']).not_to eq(nil)
|
||||||
expect(response_json['link']).to include(response_json['code'])
|
expect(response_json['link']).to include(response_json['code'])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should return with a 400 status and 'invalid url parameter' message when an empty url is submitted" do
|
it "should return with a 400 status and 'invalid url parameter' message when an empty url is submitted" 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')
|
||||||
|
|
@ -64,7 +63,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')
|
||||||
|
|
@ -74,7 +73,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')
|
||||||
|
|
@ -84,14 +83,13 @@ 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'
|
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
|
||||||
message when a request is made with the wrong content type header" do
|
|
||||||
data = {
|
data = {
|
||||||
url: 'http://google.com'
|
url: 'http://google.com'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue