From 03399ab51c0d12af33a24134db66804a8dda0074 Mon Sep 17 00:00:00 2001 From: Andrew Tomaka Date: Tue, 21 Jul 2015 10:33:31 -0400 Subject: [PATCH] Initial commit --- .gitignore | 2 + .gitlab-ci.yml | 37 +++++ .ruby-version | 1 + Dockerfile | 17 +++ Gemfile | 26 ++++ Gemfile.lock | 143 ++++++++++++++++++ Makefile | 21 +++ README.md | 3 + Rakefile | 7 + app.rb | 89 +++++++++++ config.ru | 2 + config/app.yml.sample | 3 + config/environments.rb | 15 ++ config/init.rb | 17 +++ db/.gitkeep | 0 db/migrate/20150721142956_add_links.rb | 11 ++ .../20150721201947_change_link_to_url.rb | 5 + db/schema.rb | 24 +++ helpers/application_helper.rb | 20 +++ models/link.rb | 27 ++++ public/custom.css | 17 +++ public/custom.js | 10 ++ spec/factories/link_factory.rb | 6 + spec/features/admin/create_link_spec.rb | 49 ++++++ spec/features/admin/delete_link_spec.rb | 36 +++++ spec/features/admin/list_links_spec.rb | 53 +++++++ spec/features/admin/send_link_spec.rb | 34 +++++ spec/features/authorization_spec.rb | 56 +++++++ spec/features/calendar_spec.rb | 17 +++ spec/fixtures/app.yml | 3 + spec/models/link_spec.rb | 65 ++++++++ spec/spec_helper.rb | 72 +++++++++ views/calendar.slim | 1 + views/form_errors.slim | 4 + views/layout.slim | 33 ++++ views/link_list.slim | 14 ++ views/manage.slim | 15 ++ views/new.slim | 9 ++ 38 files changed, 964 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .ruby-version create mode 100644 Dockerfile create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 Makefile create mode 100644 README.md create mode 100644 Rakefile create mode 100644 app.rb create mode 100644 config.ru create mode 100644 config/app.yml.sample create mode 100644 config/environments.rb create mode 100644 config/init.rb create mode 100644 db/.gitkeep create mode 100644 db/migrate/20150721142956_add_links.rb create mode 100644 db/migrate/20150721201947_change_link_to_url.rb create mode 100644 db/schema.rb create mode 100644 helpers/application_helper.rb create mode 100644 models/link.rb create mode 100644 public/custom.css create mode 100644 public/custom.js create mode 100644 spec/factories/link_factory.rb create mode 100644 spec/features/admin/create_link_spec.rb create mode 100644 spec/features/admin/delete_link_spec.rb create mode 100644 spec/features/admin/list_links_spec.rb create mode 100644 spec/features/admin/send_link_spec.rb create mode 100644 spec/features/authorization_spec.rb create mode 100644 spec/features/calendar_spec.rb create mode 100644 spec/fixtures/app.yml create mode 100644 spec/models/link_spec.rb create mode 100644 spec/spec_helper.rb create mode 100644 views/calendar.slim create mode 100644 views/form_errors.slim create mode 100644 views/layout.slim create mode 100644 views/link_list.slim create mode 100644 views/manage.slim create mode 100644 views/new.slim diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2de85d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +db/*.db +config/app.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..0695621 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,37 @@ +stages: + - test + - build + - deploy + +test: + stage: test + script: + - apt-get update -qy + - apt-get install -y nodejs libqtwebkit-dev qt4-qmake sqlite3 libsqlite3-dev xvfb + - bundle install --path /cache + - RACK_ENV=test bundle exec rake db:create + - RACK_ENV=test bundle exec rake db:migrate + - RACK_ENV=test xvfb-run -a bundle exec rspec + tags: + - rails +build: + stage: build + script: + - docker build -t atomaka/link-share . + except: + - tags + tags: + - docker +deploy: + stage: deploy + script: + - VERSION=$(git describe --tags) + - docker build -t atomaka/link-share . + - docker tag atomaka/link-share:latest docker.atomaka.com/atomaka/link-share:$VERSION + - docker tag atomaka/link-share:latest docker.atomaka.com/atomaka/link-share:latest + - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD -e me@atomaka.com docker.atomaka.com + - docker push docker.atomaka.com/atomaka/link-share + only: + - tags + tags: + - docker diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..b1b25a5 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.2.2 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..39d0b60 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM alpine:latest + +RUN export LANG=en_US.UTF-8 && \ + export LANGUAGE=en_US.UTF-8 && \ + export LC_ALL=en_US.UTF-8 + +RUN apk update \ + && apk add build-base ruby-dev sqlite-dev \ + && apk add ruby ruby-bundler ruby-io-console \ + && rm -rf /var/cache/apk* + +WORKDIR /app +COPY Gemfile* ./ +RUN bundle install --path=vendor/bundle --jobs=4 --without=development test +COPY . /app + +CMD bundle exec rackup -o 0.0.0.0 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..f144423 --- /dev/null +++ b/Gemfile @@ -0,0 +1,26 @@ +source 'https://rubygems.org' + +gem 'activerecord' +gem 'sinatra' +gem 'sqlite3' + +gem 'sinatra-activerecord' +gem 'sinatra-contrib', require: false +gem 'sinatra-flash' +gem 'validate_url' + +gem 'slim' + +gem 'bigdecimal' +# alpine linux does not include a method to determine the timezone +gem 'tzinfo-data' + +group :development do + gem 'rspec' + gem 'capybara-webkit' + gem 'factory_girl' + gem 'database_cleaner' + gem 'launchy' + gem 'pry' + gem 'rerun' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..84fc6a7 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,143 @@ +GEM + remote: https://rubygems.org/ + specs: + activemodel (4.2.3) + activesupport (= 4.2.3) + builder (~> 3.1) + activerecord (4.2.3) + activemodel (= 4.2.3) + activesupport (= 4.2.3) + arel (~> 6.0) + activesupport (4.2.3) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + addressable (2.3.8) + arel (6.0.2) + backports (3.6.6) + bigdecimal (1.2.7) + builder (3.2.2) + capybara (2.5.0) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + capybara-webkit (1.7.1) + capybara (>= 2.3.0, < 2.6.0) + json + celluloid (0.16.0) + timers (~> 4.0.0) + coderay (1.1.0) + database_cleaner (1.4.1) + diff-lcs (1.2.5) + factory_girl (4.5.0) + activesupport (>= 3.0.0) + ffi (1.9.10) + hitimes (1.2.2) + i18n (0.7.0) + json (1.8.3) + launchy (2.4.3) + addressable (~> 2.3) + listen (2.10.1) + celluloid (~> 0.16.0) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + method_source (0.8.2) + mime-types (3.0) + mime-types-data (~> 3.2015) + mime-types-data (3.2015.1120) + mini_portile2 (2.0.0) + minitest (5.7.0) + multi_json (1.11.2) + nokogiri (1.6.7) + mini_portile2 (~> 2.0.0.rc2) + pry (0.10.1) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + rack (1.6.4) + rack-protection (1.5.3) + rack + rack-test (0.6.3) + rack (>= 1.0) + rb-fsevent (0.9.5) + rb-inotify (0.9.5) + ffi (>= 0.5.0) + rerun (0.10.0) + listen (~> 2.7, >= 2.7.3) + rspec (3.4.0) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) + rspec-core (3.4.1) + rspec-support (~> 3.4.0) + rspec-expectations (3.4.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.4.0) + rspec-mocks (3.4.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.4.0) + rspec-support (3.4.1) + sinatra (1.4.6) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (>= 1.3, < 3) + sinatra-activerecord (2.0.6) + activerecord (>= 3.2) + sinatra (~> 1.0) + sinatra-contrib (1.4.6) + backports (>= 2.0) + multi_json + rack-protection + rack-test + sinatra (~> 1.4.0) + tilt (>= 1.3, < 3) + sinatra-flash (0.3.0) + sinatra (>= 1.0.0) + slim (3.0.6) + temple (~> 0.7.3) + tilt (>= 1.3.3, < 2.1) + slop (3.6.0) + sqlite3 (1.3.10) + temple (0.7.6) + thread_safe (0.3.5) + tilt (2.0.1) + timers (4.0.1) + hitimes + tzinfo (1.2.2) + thread_safe (~> 0.1) + tzinfo-data (1.2015.7) + tzinfo (>= 1.0.0) + validate_url (1.0.2) + activemodel (>= 3.0.0) + addressable + xpath (2.0.0) + nokogiri (~> 1.3) + +PLATFORMS + ruby + +DEPENDENCIES + activerecord + bigdecimal + capybara-webkit + database_cleaner + factory_girl + launchy + pry + rerun + rspec + sinatra + sinatra-activerecord + sinatra-contrib + sinatra-flash + slim + sqlite3 + tzinfo-data + validate_url + +BUNDLED WITH + 1.10.6 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1c389df --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +NAME = atomaka/docker-linkshare +CONTAINER = sinatra-linkshare +DOMAIN = linkshare.atomaka.com +PORT = 10081 +DATABASE = /home/atomaka/linkshare.db + +all: build + +build: + docker build -t $(NAME) . + +clean: + docker rm -f $(CONTAINER) + +deploy: clean + touch $(DATABASE) + docker run -e VIRTUAL_HOST=$(DOMAIN) -d -p $(PORT):9292 -v $(DATABASE):/app/db/linkshare.db --name=$(CONTAINER) --restart=always $(NAME) + docker exec $(CONTAINER) rake db:migrate + +update: + git pull origin master diff --git a/README.md b/README.md new file mode 100644 index 0000000..26921b4 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +[![build status](https://git.atomaka.com/ci/projects/2/status.png?ref=master)](https://git.atomaka.com/ci/projects/2?ref=master) + +# link-share diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..1134fa0 --- /dev/null +++ b/Rakefile @@ -0,0 +1,7 @@ +require './app' +require 'sinatra/activerecord/rake' + +desc "Starts the development server" +task :serve do + `bundle exec rerun -b rackup` +end diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..4ab628e --- /dev/null +++ b/app.rb @@ -0,0 +1,89 @@ +require './config/init' + +get '/manage' do + protected! + + @links = get_links(params[:status]) + + slim :manage +end + +get '/new' do + protected! + + @link = Link.new + + slim :new +end + +post '/' do + protected! + + @link = Link.new(params[:link]) + + if @link.save + flash[:success] = 'Link has been created' + redirect '/manage' + else + flash.now[:danger] = 'Did not pass validations' + slim :new + end +end + +get '/send' do + protected! + + @link = Link.find(params[:id]) + + @link.mark_sent + + flash[:success] = 'Link has been marked as sent' + redirect '/manage' +end + +get '/destroy' do + protected! + + @link = Link.find(params[:id]) + + if @link.sent? + flash[:warning] = 'Cannot delete sent link' + else + @link.destroy + flash[:success] = 'Link has been deleted' + end + + redirect '/manage' +end + +get '/' do + slim :calendar +end + +get '/events' do + start = params[:start] + finish = params[:end] + + json serialize(Link.calendar(start, finish)) +end + +private + def get_links(status) + if status == 'sent' + Link.sent + elsif status == 'all' + Link.all + else + Link.unsent + end + end + + def serialize(events) + events.map do |event| + { + title: event.title, + url: event.url, + start: event.sent_at.in_time_zone('Eastern Time (US & Canada)') + } + end + end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..76a6edf --- /dev/null +++ b/config.ru @@ -0,0 +1,2 @@ +require './app' +run Sinatra::Application diff --git a/config/app.yml.sample b/config/app.yml.sample new file mode 100644 index 0000000..321ffd1 --- /dev/null +++ b/config/app.yml.sample @@ -0,0 +1,3 @@ +secret: YOUR_SECRET +users: + USERNAME: PASSWORD diff --git a/config/environments.rb b/config/environments.rb new file mode 100644 index 0000000..95c9e8f --- /dev/null +++ b/config/environments.rb @@ -0,0 +1,15 @@ +set :database, 'sqlite3:db/linkshare.db' + +configure :production do + config_file 'config/app.yml' +end + +configure :development do + set :show_exceptions, true + config_file 'config/app.yml' +end + +configure :test do + set :database, 'sqlite3:db/test.db' + config_file 'spec/fixtures/app.yml' +end diff --git a/config/init.rb b/config/init.rb new file mode 100644 index 0000000..c3d1df5 --- /dev/null +++ b/config/init.rb @@ -0,0 +1,17 @@ +require 'rubygems' +require 'bundler' +require 'bundler/setup' +require 'sinatra/config_file' +require 'sinatra/json' +Bundler.require + +set :root, File.dirname('..') + +require_relative 'environments' + +require_relative '../models/link' +require_relative '../helpers/application_helper' + +config_file 'config/app.yml' + +use Rack::Session::Cookie, secret: settings.secret diff --git a/db/.gitkeep b/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/db/migrate/20150721142956_add_links.rb b/db/migrate/20150721142956_add_links.rb new file mode 100644 index 0000000..38c7b00 --- /dev/null +++ b/db/migrate/20150721142956_add_links.rb @@ -0,0 +1,11 @@ +class AddLinks < ActiveRecord::Migration + def change + create_table :links do |t| + t.string :title + t.string :link + t.datetime :sent_at + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20150721201947_change_link_to_url.rb b/db/migrate/20150721201947_change_link_to_url.rb new file mode 100644 index 0000000..0d09320 --- /dev/null +++ b/db/migrate/20150721201947_change_link_to_url.rb @@ -0,0 +1,5 @@ +class ChangeLinkToUrl < ActiveRecord::Migration + def change + rename_column :links, :link, :url + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..a96a86e --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,24 @@ +# 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: 20150721201947) do + + create_table "links", force: :cascade do |t| + t.string "title" + t.string "url" + t.datetime "sent_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + +end diff --git a/helpers/application_helper.rb b/helpers/application_helper.rb new file mode 100644 index 0000000..678ba9d --- /dev/null +++ b/helpers/application_helper.rb @@ -0,0 +1,20 @@ +helpers do + def protected! + return if authorized? + headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"' + halt 401, "Not authorized\n" + end + + def authorized? + @auth ||= Rack::Auth::Basic::Request.new(request.env) + return unless @auth.provided? and @auth.basic? + + username, password = @auth.credentials + + user_exists?(username) && settings.users[username] == password + end + + def user_exists?(username) + settings.users.keys.include?(username) + end +end diff --git a/models/link.rb b/models/link.rb new file mode 100644 index 0000000..02128f5 --- /dev/null +++ b/models/link.rb @@ -0,0 +1,27 @@ +class Link < ActiveRecord::Base + validates :title, + presence: true + validates :url, + presence: true, + url: true, + uniqueness: true + + scope :sent, -> { where('sent_at IS NOT NULL').order('sent_at DESC') } + scope :unsent, -> { where('sent_at IS NULL').order('created_at ASC') } + scope :sent_after, ->(date) { where('sent_at > ?', date) } + scope :sent_before, ->(date) { where('sent_at < ?', date) } + scope :calendar, ->(start, finish) { sent_after(start).sent_before(finish) } + + def mark_sent + update_attribute(:sent_at, Time.now) + end + + def sent? + !!self.sent_at + end + + def url=(url) + url.chomp!('/') if url.respond_to?(:chomp) + write_attribute(:url, url) + end +end diff --git a/public/custom.css b/public/custom.css new file mode 100644 index 0000000..0e59572 --- /dev/null +++ b/public/custom.css @@ -0,0 +1,17 @@ +.link { + padding: 5px; + background: #fff; +} + +.link:nth-of-type(odd) { + background: #e7e7e7 +} + +.controls { + margin-bottom: 10px; + margin-top: 10px; +} + +.fc-time { + display: none; +} diff --git a/public/custom.js b/public/custom.js new file mode 100644 index 0000000..dfb933c --- /dev/null +++ b/public/custom.js @@ -0,0 +1,10 @@ +$(document).ready(function() { + $('#calendar').fullCalendar({ + header: { + left: 'prev,next today', + center: 'title', + right: '' + }, + events: '/events', + }); +}); diff --git a/spec/factories/link_factory.rb b/spec/factories/link_factory.rb new file mode 100644 index 0000000..92d9d02 --- /dev/null +++ b/spec/factories/link_factory.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :link do + sequence(:title) { |n| "Sequenced Title (#{n})" } + sequence(:url) { |n| "http://www.#{n}example#{n}.com/#{n}" } + end +end diff --git a/spec/features/admin/create_link_spec.rb b/spec/features/admin/create_link_spec.rb new file mode 100644 index 0000000..be06c32 --- /dev/null +++ b/spec/features/admin/create_link_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe 'Admin Create Links' do + context 'when not logged in' do + it 'should not allow access to new link form' do + visit '/new' + expect(page.status_code).to be(401) + end + end + + context 'when logged in' do + before(:each) { basic_auth 'admin', 'password' } + + context 'with valid data' do + let(:link) { build(:link) } + + it 'should allow creation of a link' do + visit '/new' + fill_in :link_title, with: link.title + fill_in :link_url, with: link.url + click_button 'Submit' + + expect(page).to have_content 'Link has been created' + end + + it 'should list the new link' do + visit '/new' + fill_in :link_title, with: link.title + fill_in :link_url, with: link.url + click_button 'Submit' + + expect(page).to have_content link.title + end + end + + context 'with invalid data' do + let(:link) { build(:link, title: '') } + + it 'should not allow link with invalid data' do + visit '/new' + fill_in :link_title, with: link.title + fill_in :link_url, with: link.url + click_button 'Submit' + + expect(page).to have_content 'Did not pass validations' + end + end + end +end diff --git a/spec/features/admin/delete_link_spec.rb b/spec/features/admin/delete_link_spec.rb new file mode 100644 index 0000000..9b3426f --- /dev/null +++ b/spec/features/admin/delete_link_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe 'Admin Delete Links' do + context 'when not logged in' do + it 'should not allow access to delete links' do + visit '/send' + expect(page.status_code).to be(401) + end + end + + context 'when logged in' do + let!(:links) { 10.times.collect { create(:link) } } + before(:each) { basic_auth 'admin', 'password' } + + it 'should allow deleting of a link' do + visit '/manage' + first('a', text: 'Delete').click + expect(page).to have_content('Link has been deleted') + end + + it 'should remove deleted link all lists' do + link = create(:link) + + visit '/manage' + + find('a', text: link.title) + .find(:xpath, '../..') + .find('a', text: 'Delete') + .click + + visit '/manage?status=all' + + expect(page).to_not have_content(link.title) + end + end +end diff --git a/spec/features/admin/list_links_spec.rb b/spec/features/admin/list_links_spec.rb new file mode 100644 index 0000000..62878bb --- /dev/null +++ b/spec/features/admin/list_links_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe 'Admin List Links' do + context 'when not logged in' do + it 'should not allow access to list links' do + visit '/manage' + expect(page.status_code).to be(401) + end + end + + context 'when logged in' do + let!(:sent_link) { create(:link, sent_at: Time.now) } + let!(:links) { 10.times.collect { create(:link) } } + before(:each) { basic_auth 'admin', 'password' } + + context 'unsent' do + it 'should show unsent links by default' do + visit '/manage' + + expect(page).to_not have_content(sent_link.title) + links.each { |link| expect(page).to have_content(link.title) } + end + + it 'should only list unsent links' do + visit '/manage' + first('a', text: 'Unsent Links').click + + expect(page).to_not have_content(sent_link.title) + links.each { |link| expect(page).to have_content(link.title) } + end + end + + context 'sent' do + it 'should only list sent links' do + visit '/manage' + first('a', text: 'Sent Links').click + + expect(page).to have_content(sent_link.title) + links.each { |link| expect(page).to_not have_content(link.title) } + end + end + + context 'all' do + it 'should list all links' do + visit '/manage' + first('a', text: 'All Links').click + + expect(page).to have_content(sent_link.title) + links.each { |link| expect(page).to have_content(link.title) } + end + end + end +end diff --git a/spec/features/admin/send_link_spec.rb b/spec/features/admin/send_link_spec.rb new file mode 100644 index 0000000..571a583 --- /dev/null +++ b/spec/features/admin/send_link_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe 'Admin Send Links' do + context 'when not logged in' do + it 'should not allow access to send links' do + visit '/send' + expect(page.status_code).to be(401) + end + end + + context 'when logged in' do + let!(:links) { 10.times.collect { create(:link) } } + before(:each) { basic_auth 'admin', 'password' } + + it 'should allow sending of a link' do + visit '/manage' + first('a', text: 'Send').click + expect(page).to have_content('Link has been marked as sent') + end + + it 'should remove sent link from unsent list' do + link = create(:link) + + visit '/manage' + + find('a', text: link.title) + .find(:xpath, '../..') + .find('a', text: 'Send') + .click + + expect(page).to_not have_content(link.title) + end + end +end diff --git a/spec/features/authorization_spec.rb b/spec/features/authorization_spec.rb new file mode 100644 index 0000000..db825a1 --- /dev/null +++ b/spec/features/authorization_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe 'Authorization' do + context 'when not logged in' do + it 'should not allow access to manage' do + visit '/manage' + expect(page.status_code).to be(401) + end + + it 'should not allow access to the create form' do + visit '/new' + expect(page.status_code).to be(401) + end + + it 'should not allow access to send' do + visit '/send' + expect(page.status_code).to be(401) + end + + it 'should not allow access to delete' do + visit '/destroy' + expect(page.status_code).to be(401) + end + end + + it 'should allow accessing the home page' do + visit '/' + expect(page.status_code).to be(200) + end + + it 'should allow accessing the events page' do + visit '/events' + expect(page.status_code).to be(200) + end + + context 'when logging in' do + context 'with incorrect credentials' do + before(:each) { basic_auth 'baduser', 'badpassword' } + + it 'should allow not allow access' do + visit '/manage' + expect(page.status_code).to be(401) + end + end + + context 'with correct credentials' do + before(:each) { basic_auth 'admin', 'password' } + + it 'should allow access' do + visit '/manage' + + expect(page.status_code).to be(200) + end + end + end +end diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb new file mode 100644 index 0000000..c6c11ef --- /dev/null +++ b/spec/features/calendar_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe 'Calendar View' do + OVER_ONE_MONTH = 60*60*24*40 + + let!(:showing_link) { create(:link, sent_at: Time.now) } + let!(:before_link) { create(:link, sent_at: Time.now - OVER_ONE_MONTH) } + let!(:after_link) { create(:link, sent_at: Time.now + OVER_ONE_MONTH) } + + it 'should only show links for the current month', js: true do + visit '/' + + expect(page).to have_content(showing_link.title) + expect(page).to_not have_content(before_link.title) + expect(page).to_not have_content(after_link.title) + end +end diff --git a/spec/fixtures/app.yml b/spec/fixtures/app.yml new file mode 100644 index 0000000..49e8dc2 --- /dev/null +++ b/spec/fixtures/app.yml @@ -0,0 +1,3 @@ +secret: my_secret_something_blan +users: + admin: password diff --git a/spec/models/link_spec.rb b/spec/models/link_spec.rb new file mode 100644 index 0000000..682b87f --- /dev/null +++ b/spec/models/link_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Link do + let(:link) { build(:link) } + + context 'with valid data' do + it 'should be valid' do + expect(link).to be_valid + end + end + + context 'with invalid data' do + it 'should not be valid with blank title' do + link.title = '' + + expect(link).to be_invalid + end + + it 'should not be valid with a blank url' do + link.url = '' + + expect(link).to be_invalid + end + + it 'should not be valid with a bad url' do + link.url = 'bad' + + expect(link).to be_invalid + end + + it 'should not be valid with a duplicate url' do + create(:link, url: link.url) + + expect(link).to be_invalid + end + end + + context '#mark_sent' do + it 'should set a time to sent_at' do + link.mark_sent + + expect(link.sent_at).to_not be(nil) + end + end + + context '#sent?' do + context 'when already sent' do + it 'should respond with true' do + link.mark_sent + expect(link.sent?).to be(true) + end + + it 'should respond with false' do + expect(link.sent?).to be(false) + end + end + end + + context '#url' do + it 'removes a trailing slash' do + link.url = 'http://www.url.com/' + expect(link.url).to eq('http://www.url.com') + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..4f7ca7e --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,72 @@ +require 'rack/test' +require 'rspec' +require 'capybara/rspec' +require 'capybara/webkit' +require 'factory_girl' +require 'database_cleaner' + +ENV['RACK_ENV'] = 'test' + +require File.expand_path '../../app.rb', __FILE__ + +ActiveRecord::Migration.maintain_test_schema! + +module TestingMixin + include Rack::Test::Methods + include RSpec::Matchers + include Capybara::DSL + include FactoryGirl::Syntax::Methods + + Capybara.app = Sinatra::Application + Capybara.javascript_driver = :webkit + Capybara.asset_host = 'http://localhost:3000' + + FactoryGirl.definition_file_paths = %w{./factories ./test/factories ./spec/factories} + FactoryGirl.find_definitions + + def app() Sinatra::Application end + + def basic_auth(username, password) + if page.driver.respond_to?(:basic_auth) + page.driver.basic_auth(username, password) + elsif page.driver.respond_to?(:basic_authorize) + page.driver.basic_authorize(username, password) + elsif page.driver.respond_to?(:browser) && page.driver.browser.respond_to?(:basic_authorize) + page.driver.browser.basic_authorize(username, password) + else + raise "I don't know how to log in!" + end + end +end + +Capybara::Webkit.configure do |config| + config.allow_unknown_urls +end + +RSpec.configure do |config| + config.include TestingMixin + + config.before(:suite) do + DatabaseCleaner.clean_with(:truncation) + end + + config.before(:each) do + DatabaseCleaner.strategy = :truncation + end + + config.before(:each, :js => true) do + DatabaseCleaner.strategy = :truncation + end + + config.before(:each) do + DatabaseCleaner.start + end + + config.append_after(:each) do + DatabaseCleaner.clean + Capybara.reset_sessions! + end + + config.order = :random + Kernel.srand config.seed +end diff --git a/views/calendar.slim b/views/calendar.slim new file mode 100644 index 0000000..114bf52 --- /dev/null +++ b/views/calendar.slim @@ -0,0 +1 @@ +#calendar diff --git a/views/form_errors.slim b/views/form_errors.slim new file mode 100644 index 0000000..bad3ced --- /dev/null +++ b/views/form_errors.slim @@ -0,0 +1,4 @@ +- if @link.errors.any? + ul + - @link.errors.full_messages.each do |error| + li= error diff --git a/views/layout.slim b/views/layout.slim new file mode 100644 index 0000000..c8f8515 --- /dev/null +++ b/views/layout.slim @@ -0,0 +1,33 @@ +doctype html +html + head + title Links App + link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" + link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.min.css" + link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.print.css" media="print" + link rel="stylesheet" href="/custom.css" + body + .navbar.navbar-default.navbar-static-top.navbar-custom + .container + .navbar-header + button.navbar-toggle.collapsed type='button' data-toggle='collapse' data-target='.navbar-collapse' + span.sr-only Toggle navigation + span.icon-bar + span.icon-bar + span.icon-bar + a href="/" class="navbar-brand" Links + .collapse.navbar-collapse + ul.nav.navbar-nav + li + a href="/manage" Manage + ul.nav.navbar-nav.navbar-right + .container + - unless flash.empty? + - flash.each_key do |key| + .alert class="alert-#{key}" role="alert"= flash[key] + == yield + script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js" + script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js" + script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js" + script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.min.js" + script src="custom.js" diff --git a/views/link_list.slim b/views/link_list.slim new file mode 100644 index 0000000..8917f64 --- /dev/null +++ b/views/link_list.slim @@ -0,0 +1,14 @@ +.links +- @links.each do |link| + .row.link + .col-md-9 + a href="#{link.url}" #{link.title} + .col-md-3 + - if link.sent_at + = link.sent_at + - else + ul.list-inline style="margin: 0" + li + a href="/send?id=#{link.id}" Send + li + a href="/destroy?id=#{link.id}" Delete diff --git a/views/manage.slim b/views/manage.slim new file mode 100644 index 0000000..72aa3d2 --- /dev/null +++ b/views/manage.slim @@ -0,0 +1,15 @@ +.row.controls + .btn-toolbar.pull-right + a href="/manage?status=unsent" class="btn btn-primary" Unsent Links + a href="/manage?status=sent" class="btn btn-primary" Sent Links + a href="/manage?status=all" class="btn btn-primary" All Links + a href="/new" class="btn btn-primary" New Link + +== slim :link_list + +.row.controls + .btn-toolbar + a href="/manage?status=unsent" class="btn btn-primary" Unsent Links + a href="/manage?status=sent" class="btn btn-primary" Sent Links + a href="/manage?status=all" class="btn btn-primary" All Links + a href="/new" class="btn btn-primary" New Link diff --git a/views/new.slim b/views/new.slim new file mode 100644 index 0000000..bc9255e --- /dev/null +++ b/views/new.slim @@ -0,0 +1,9 @@ +== slim :form_errors +form action="/" method="post" + .form-group + label for="link_title" Title + input type="text" name="link[title]" id="link_title" class="form-control" value="#{@link.title}" + .form-group + label for="link_url" Link + input type="text" name="link[url]" id="link_url" class="form-control" value="#{@link.url}" + button type="submit" class="btn btn-primary" Submit