fix migrations, acceptance testing for auth, 32 char username limit

This commit is contained in:
Kyle Drake 2014-04-01 16:34:53 -07:00
parent fdd4017523
commit 5dfc715148
No known key found for this signature in database
GPG key ID: 8BE721072E1864BE
18 changed files with 247 additions and 125 deletions

1
.gitignore vendored
View file

@ -20,3 +20,4 @@ tests/coverage
config.yml config.yml
.DS_Store .DS_Store
domains domains
public/sites_test

View file

@ -52,6 +52,10 @@ group :test do
gem 'webmock' gem 'webmock'
gem 'mocha', require: nil gem 'mocha', require: nil
gem 'rake', require: nil gem 'rake', require: nil
gem 'poltergeist'
gem 'phantomjs', require: 'phantomjs/poltergeist'
gem 'capybara'
gem 'capybara_minitest_spec'
platform :mri do platform :mri do
gem 'simplecov', require: nil gem 'simplecov', require: nil

View file

@ -6,10 +6,20 @@ GEM
ansi (1.4.3) ansi (1.4.3)
bcrypt (3.1.7) bcrypt (3.1.7)
builder (3.2.2) builder (3.2.2)
capybara (2.2.1)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
capybara_minitest_spec (1.0.1)
capybara (>= 2)
minitest (>= 2)
celluloid (0.15.2) celluloid (0.15.2)
timers (~> 1.1.0) timers (~> 1.1.0)
childprocess (0.5.2) childprocess (0.5.2)
ffi (~> 1.0, >= 1.0.11) ffi (~> 1.0, >= 1.0.11)
cliver (0.3.2)
coderay (1.1.0) coderay (1.1.0)
columnize (0.3.6) columnize (0.3.6)
connection_pool (2.0.0) connection_pool (2.0.0)
@ -39,6 +49,7 @@ GEM
metaclass (0.0.4) metaclass (0.0.4)
method_source (0.8.2) method_source (0.8.2)
mime-types (1.25.1) mime-types (1.25.1)
mini_portile (0.5.3)
minitest (5.3.1) minitest (5.3.1)
minitest-reporters (1.0.2) minitest-reporters (1.0.2)
ansi ansi
@ -48,7 +59,15 @@ GEM
mocha (1.0.0) mocha (1.0.0)
metaclass (~> 0.0.1) metaclass (~> 0.0.1)
multi_json (1.9.2) multi_json (1.9.2)
nokogiri (1.6.1)
mini_portile (~> 0.5.0)
pg (0.17.1) pg (0.17.1)
phantomjs (1.9.7.0)
poltergeist (1.5.0)
capybara (~> 2.1)
cliver (~> 0.3.1)
multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
polyglot (0.3.4) polyglot (0.3.4)
powerbar (1.0.11) powerbar (1.0.11)
ansi (~> 1.4.0) ansi (~> 1.4.0)
@ -129,6 +148,9 @@ GEM
addressable (>= 2.2.7) addressable (>= 2.2.7)
crack (>= 0.3.2) crack (>= 0.3.2)
websocket (1.0.7) websocket (1.0.7)
websocket-driver (0.3.2)
xpath (2.0.0)
nokogiri (~> 1.3)
PLATFORMS PLATFORMS
ruby ruby
@ -136,6 +158,8 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
ago ago
bcrypt bcrypt
capybara
capybara_minitest_spec
fabrication fabrication
faker faker
hiredis hiredis
@ -148,6 +172,8 @@ DEPENDENCIES
minitest-reporters minitest-reporters
mocha mocha
pg pg
phantomjs
poltergeist
pry pry
pry-debugger pry-debugger
puma puma

View file

@ -7,7 +7,7 @@ end
desc "Run all tests" desc "Run all tests"
Rake::TestTask.new do |t| Rake::TestTask.new do |t|
t.libs << "spec" t.libs << "spec"
t.test_files = FileList['tests/*_test.rb'] t.test_files = FileList['tests/*_tests.rb']
t.verbose = true t.verbose = true
end end

19
app.rb
View file

@ -103,12 +103,12 @@ end
get '/blog' do get '/blog' do
# expires 500, :public, :must_revalidate # expires 500, :public, :must_revalidate
return File.read File.join(DIR_ROOT, 'public', 'sites', 'blog', 'index.html') return File.read File.join(Sites::SITE_FILES_ROOT, 'blog', 'index.html')
end end
get '/blog/:article' do |article| get '/blog/:article' do |article|
# expires 500, :public, :must_revalidate # expires 500, :public, :must_revalidate
path = File.join DIR_ROOT, 'public', 'sites', 'blog', "#{article}.html" path = File.join Sites::SITE_FILES_ROOT, 'blog', "#{article}.html"
pass if !File.exist?(path) pass if !File.exist?(path)
File.read path File.read path
end end
@ -137,9 +137,16 @@ end
post '/create' do post '/create' do
dashboard_if_signed_in dashboard_if_signed_in
@site = Site.new username: params[:username], password: params[:password], email: params[:email], new_tags: params[:tags], is_nsfw: params[:is_nsfw], ip: request.ip @site = Site.new(
username: params[:username],
password: params[:password],
email: params[:email],
new_tags: params[:tags],
is_nsfw: params[:is_nsfw],
ip: request.ip
)
recaptcha_is_valid = recaptcha_valid? recaptcha_is_valid = ENV['RACK_ENV'] == 'test' || recaptcha_valid?
if @site.valid? && recaptcha_is_valid if @site.valid? && recaptcha_is_valid
@ -148,7 +155,7 @@ post '/create' do
DB.transaction { DB.transaction {
@site.save @site.save
FileUtils.mkdir base_path FileUtils.mkdir_p base_path
File.write File.join(base_path, 'index.html'), slim(:'templates/index', pretty: true, layout: false) File.write File.join(base_path, 'index.html'), slim(:'templates/index', pretty: true, layout: false)
File.write File.join(base_path, 'not_found.html'), slim(:'templates/not_found', pretty: true, layout: false) File.write File.join(base_path, 'not_found.html'), slim(:'templates/not_found', pretty: true, layout: false)
@ -661,7 +668,7 @@ def current_site
end end
def site_base_path(subname) def site_base_path(subname)
File.join settings.public_folder, 'sites', subname File.join Site::SITE_FILES_ROOT, subname
end end
def site_file_path(filename) def site_file_path(filename)

View file

@ -4,6 +4,6 @@ Sequel.migration do
} }
down { down {
DB.add_column :sites, :hits DB.drop_column :sites, :hits
} }
end end

View file

@ -4,6 +4,6 @@ Sequel.migration do
} }
down { down {
DB.add_column :sites, :is_admin DB.drop_column :sites, :is_admin
} }
end end

View file

@ -4,6 +4,6 @@ Sequel.migration do
} }
down { down {
DB.add_column :sites, :is_banned DB.drop_column :sites, :is_banned
} }
end end

View file

@ -4,6 +4,6 @@ Sequel.migration do
} }
down { down {
DB.add_column :sites, :ip DB.drop_column :sites, :ip
} }
end end

View file

@ -6,6 +6,5 @@ Sequel.migration do
down { down {
DB.drop_column :sites, :password_reset_token DB.drop_column :sites, :password_reset_token
DB.drop_index :sites, :password_reset_token
} }
end end

View file

@ -6,6 +6,5 @@ Sequel.migration do
down { down {
DB.drop_column :sites, :site_changed DB.drop_column :sites, :site_changed
DB.drop_index :sites, :site_changed
} }
end end

View file

@ -6,6 +6,5 @@ Sequel.migration do
down { down {
DB.drop_column :sites, :changed_count DB.drop_column :sites, :changed_count
DB.drop_index :sites, :changed_count
} }
end end

View file

@ -1,11 +1,38 @@
class Site < Sequel::Model class Site < Sequel::Model
# We might need to include fonts in here.. # We might need to include fonts in here..
VALID_MIME_TYPES = ['text/plain', 'text/html', 'text/css', 'application/javascript', 'image/png', 'image/jpeg', 'image/gif', 'image/svg+xml', 'application/vnd.ms-fontobject', 'application/x-font-ttf', 'application/octet-stream', 'text/csv', 'text/tsv', 'text/cache-manifest', 'image/x-icon', 'application/pdf', 'application/pgp-keys', 'text/xml', 'application/xml', 'audio/midi'] VALID_MIME_TYPES = %w{
VALID_EXTENSIONS = %w{ html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff json geojson csv tsv mf ico pdf asc key pgp xml mid midi } text/plain
#USERNAME_SHITLIST = %w{ payment secure login signin www ww web } # I thought they were funny personally, but everybody is freaking out so.. text/html
text/css
application/javascript
image/png
image/jpeg
image/gif
image/svg+xml
application/vnd.ms-fontobject
application/x-font-ttf
application/octet-stream
text/csv
text/tsv
text/cache-manifest
image/x-icon
application/pdf
application/pgp-keys
text/xml
application/xml
audio/midi
}
VALID_EXTENSIONS = %w{
html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff json
geojson csv tsv mf ico pdf asc key pgp xml mid midi
}
MAX_SPACE = (5242880*2) # 10MB MAX_SPACE = (5242880*2) # 10MB
MINIMUM_PASSWORD_LENGTH = 5 MINIMUM_PASSWORD_LENGTH = 5
BAD_USERNAME_REGEX = /[^\w-]/i BAD_USERNAME_REGEX = /[^\w-]/i
VALID_HOSTNAME = /^[a-z0-9][a-z0-9-]+?[a-z0-9]$/i # http://tools.ietf.org/html/rfc1123
SITE_FILES_ROOT = File.join(DIR_ROOT, 'public', (ENV['RACK_ENV'] == 'test' ? 'sites_test' : 'sites'))
many_to_one :server many_to_one :server
many_to_many :tags many_to_many :tags
@ -77,13 +104,15 @@ class Site < Sequel::Model
errors.add :over_capacity, 'We are currently at capacity, and cannot create your home page. We will fix this shortly. Please come back later and try again, our apologies.' errors.add :over_capacity, 'We are currently at capacity, and cannot create your home page. We will fix this shortly. Please come back later and try again, our apologies.'
end end
if values[:username].nil? || values[:username].empty? || values[:username].match(BAD_USERNAME_REGEX) if !values[:username].match(VALID_HOSTNAME)
errors.add :username, 'A valid username is required.' errors.add :username, 'A valid user/site name is required.'
end
if values[:username].length > 32
errors.add :username, 'User/site name cannot exceed 32 characters.'
end end
# Check for existing user # Check for existing user
user = self.class.select(:id, :username).filter(username: values[:username]).first user = self.class.select(:id, :username).filter(username: values[:username]).first
if user if user
@ -109,7 +138,7 @@ class Site < Sequel::Model
end end
def file_path def file_path
File.join DIR_ROOT, 'public', 'sites', username File.join SITE_FILES_ROOT, username
end end
def file_list def file_list

144
tests/acceptance_tests.rb Normal file
View file

@ -0,0 +1,144 @@
require_relative './environment'
Capybara.app = Sinatra::Application
def teardown
Capybara.reset_sessions!
Capybara.use_default_driver
end
describe 'index' do
include Capybara::DSL
it 'goes to signup' do
visit '/'
click_button 'Create My Website'
page.must_have_content('Create a New Home Page')
end
end
describe 'signup' do
include Capybara::DSL
def fill_in_valid
@site = Fabricate.attributes_for(:site)
fill_in 'username', with: @site[:username]
fill_in 'password', with: @site[:password]
end
def visit_signup
visit '/'
click_button 'Create My Website'
end
before do
Capybara.reset_sessions!
visit_signup
end
it 'succeeds with valid data' do
fill_in_valid
click_button 'Create Home Page'
page.must_have_content 'Your Website'
assert_equal(
true,
File.exist?(File.join(Site::SITE_FILES_ROOT, @site[:username], 'index.html'))
)
end
it 'fails to create for existing site' do
fill_in_valid
click_button 'Create Home Page'
page.must_have_content 'Your Website'
Capybara.reset_sessions!
visit_signup
fill_in 'username', with: @site[:username]
fill_in 'password', with: @site[:password]
click_button 'Create Home Page'
page.must_have_content 'already taken'
end
it 'fails with missing password' do
fill_in_valid
fill_in 'password', with: ''
click_button 'Create Home Page'
page.must_have_content 'Password must be at least 5 characters'
end
it 'fails with short password' do
fill_in_valid
fill_in 'password', with: 'derp'
click_button 'Create Home Page'
page.must_have_content 'Password must be at least 5 characters'
end
it 'fails with invalid hostname for username' do
fill_in_valid
fill_in 'username', with: '|\|0p|E'
click_button 'Create Home Page'
page.current_path.must_equal '/create'
page.must_have_content 'A valid user/site name is required'
fill_in 'username', with: 'nope-'
click_button 'Create Home Page'
page.must_have_content 'A valid user/site name is required'
fill_in 'username', with: '-nope'
click_button 'Create Home Page'
page.must_have_content 'A valid user/site name is required'
end
it 'fails with username greater than 32 characters' do
fill_in_valid
fill_in 'username', with: SecureRandom.hex+'1'
click_button 'Create Home Page'
page.current_path.must_equal '/create'
page.must_have_content 'cannot exceed 32 characters'
end
end
describe 'signin' do
include Capybara::DSL
def fill_in_valid
site = Fabricate.attributes_for :site
fill_in 'username', with: site[:username]
fill_in 'password', with: site[:password]
end
before do
Capybara.reset_sessions!
end
it 'fails for invalid login' do
visit '/'
click_link 'Sign In'
page.must_have_content 'Welcome back'
fill_in_valid
click_button 'Sign in'
page.must_have_content 'Invalid login'
end
it 'fails for missing login' do
visit '/'
click_link 'Sign In'
auth = {username: SecureRandom.hex, password: Faker::Internet.password}
fill_in 'username', with: auth[:username]
fill_in 'password', with: auth[:password]
click_button 'Sign in'
page.must_have_content 'Invalid login'
end
it 'logs in with proper credentials' do
visit '/'
click_button 'Create My Website'
site = Fabricate.attributes_for(:site)
fill_in 'username', with: site[:username]
fill_in 'password', with: site[:password]
click_button 'Create Home Page'
Capybara.reset_sessions!
visit '/'
click_link 'Sign In'
fill_in 'username', with: site[:username]
fill_in 'password', with: site[:password]
click_button 'Sign in'
page.must_have_content 'Your Website'
end
end

View file

@ -1,93 +0,0 @@
require_relative './environment'
include Rack::Test::Methods
def app; App end
def status; last_response.status end
def headers; last_response.headers end
def body; last_response.body end
describe 'index' do
it 'loads' do
get '/'
status.must_equal 200
end
end
describe 'signin' do
it 'fails for missing login' do
post '/signin', username: 'derpie', password: 'lol'
fail_signin
end
it 'fails for bad password' do
@site = Fabricate :site
post '/signin', username: @site.username, password: 'derp'
fail_signin
end
it 'fails for no input' do
post '/signin'
fail_signin
end
it 'succeeds for valid input' do
password = '1tw0rkz'
@account = Fabricate :account, password: password
post '/accounts/signin', username: @account.email, password: password
headers['Location'].must_equal 'http://example.org/dashboard'
mock_dashboard_calls @account.email
get '/dashboard'
body.must_match /Dashboard/
end
end
describe 'account creation' do
it 'fails for no input' do
post '/accounts/create'
status.must_equal 200
body.must_match /There were some errors.+Valid email address is required.+Password must be/
end
it 'fails with invalid email' do
post '/accounts/create', email: 'derplol'
status.must_equal 200
body.must_match /errors.+valid email/i
end
it 'fails with invalid password' do
post '/accounts/create', 'email@example.com', password: 'sdd'
status.must_equal 200
body.must_match /errors.+Password must be at least #{Account::MINIMUM_PASSWORD_LENGTH} characters/i
end
it 'succeeds with valid info' do
account_attributes = Fabricate.attributes_for :account
mock_dashboard_calls account_attributes[:email]
post '/accounts/create', account_attributes
status.must_equal 302
headers['Location'].must_equal 'http://example.org/dashboard'
get '/dashboard'
body.must_match /Dashboard/
end
end
describe 'temporary account login' do
end
def fail_signin
headers['Location'].must_equal 'http://example.org/'
get '/'
body.must_match /invalid signin/i
end
def api_url
uri = Addressable::URI.parse $config['bitcoind_rpchost'] ? $config['bitcoind_rpchost'] : 'http://localhost'
uri.port = 8332 if uri.port.nil?
uri.user = $config['bitcoind_rpcuser'] if uri.user.nil?
uri.password = $config['bitcoind_rpcpassword'] if uri.password.nil?
"#{uri.to_s}/"
end

View file

@ -20,7 +20,7 @@ Bundler.require :test
require 'minitest/autorun' require 'minitest/autorun'
require 'sidekiq/testing/inline' require 'sidekiq/testing/inline'
Account.bcrypt_cost = BCrypt::Engine::MIN_COST Site.bcrypt_cost = BCrypt::Engine::MIN_COST
MiniTest::Reporters.use! MiniTest::Reporters::SpecReporter.new MiniTest::Reporters.use! MiniTest::Reporters::SpecReporter.new
@ -29,8 +29,11 @@ Sequel.extension :migration
Sequel::Migrator.apply DB, './migrations', 0 Sequel::Migrator.apply DB, './migrations', 0
Sequel::Migrator.apply DB, './migrations' Sequel::Migrator.apply DB, './migrations'
Server.create ip: '127.0.0.1', slots_available: 999999
Fabrication.configure do |config| Fabrication.configure do |config|
config.fabricator_path = 'tests/fabricators' config.fabricator_path = 'tests/fabricators'
config.path_prefix = DIR_ROOT config.path_prefix = DIR_ROOT
end end
I18n.enforce_available_locales = true

View file

@ -1,4 +1,4 @@
Fabricator(:site) do Fabricator(:site) do
username { Faker::Internet.email } username { SecureRandom.hex }
password { 'abcde' } password { 'abcde' }
end end

View file

@ -19,35 +19,39 @@ javascript:
h2.txt-Center Create a New Home Page h2.txt-Center Create a New Home Page
.col.col-50 style="margin:0 auto; float:none" .col.col-50 style="margin:0 auto; float:none"
p.tiny First, enter a username. This will also be used as your site path.<br><b>Do not forget this, it will be used to sign in to and manage your home page.</b> It cannot contain spaces, and can only use the following characters: a-z A-Z 0-9 _ - hr
p.tiny First, enter a username. This will also be used as your site name.<br><b>Do not forget this, it will be used to sign in to and manage your home page.</b> It can only contain letters, numbers, underscores and hyphens, and can only be 32 characters long.
br br
h5 Username h5 Username
p.tiny <input class="input-Area" name="username" type="text" placeholder="yourusername" value="#{@site.username}" autocapitalize="off" autocorrect="off">.neocities.org p.tiny <input class="input-Area" name="username" type="text" placeholder="yourusername" value="#{@site.username}" autocapitalize="off" autocorrect="off">.neocities.org
hr
p.tiny Next, enter a password. This will be used to allow you to login. Minimum 5 characters. If you don't make it a good password, Dade Murphy from the movie Hackers will come in and steal your "garbage files". p.tiny Next, enter a password. This will be used to allow you to login. Minimum 5 characters. If you don't make it a good password, Dade Murphy from the movie Hackers will come in and steal your "garbage files".
hr
h5 Password h5 Password
input class="input-Area" name="password" type="password" input class="input-Area" name="password" type="password"
br br
hr
p.tiny Now you can enter an e-mail address. Your e-mail address is private and we will not show it to anyone for any reason. You don't have to provide one, but <b>we will not be able to reset your password without it, so don't lose your username and password if you leave this blank!</b> p.tiny Now you can enter an e-mail address. Your e-mail address is private and we will not show it to anyone for any reason. You don't have to provide one, but <b>we will not be able to reset your password without it, so don't lose your username and password if you leave this blank!</b>
hr
h5 Email h5 Email
input class="input-Area" name="email" type="email" placeholder="youremail@example.com" value="#{@site.email}" input class="input-Area" name="email" type="email" placeholder="youremail@example.com" value="#{@site.email}"
br br
hr
p.tiny You can optionally enter some tags! Tags will allow others to find your site based on your interests, or your site's theme. <b>Separate multiple tags with commas</b>. Don't think too hard about this, you can change them later. You can have a maximum of ten tags, and there is a two word per tag maximum (extra words in a tag will be removed). p.tiny You can optionally enter some tags! Tags will allow others to find your site based on your interests, or your site's theme. <b>Separate multiple tags with commas</b>. Don't think too hard about this, you can change them later. You can have a maximum of ten tags, and there is a two word per tag maximum (extra words in a tag will be removed).
hr
h5 Tags h5 Tags
p: input class="input-Area" name="tags" type="text" style="width: 400px; max-width:100%" placeholder="pokemon, video games, bulbasaur" value="#{params[:tags]}" autocapitalize="off" autocorrect="off" p: input class="input-Area" name="tags" type="text" style="width: 400px; max-width:100%" placeholder="pokemon, video games, bulbasaur" value="#{params[:tags]}" autocapitalize="off" autocorrect="off"
hr
input name="is_nsfw" type="hidden" value="false" input name="is_nsfw" type="hidden" value="false"
p: strong If your page will contain objectionable (adult) content, check this box:&nbsp;&nbsp;&nbsp;<input name="is_nsfw" type="checkbox" value="true" style="margin-top:0"> p If your page will contain objectionable (adult) content, check this box:&nbsp;&nbsp;&nbsp;<input name="is_nsfw" type="checkbox" value="true" style="margin-top:0">
hr
p.tiny <b>Last thing!</b> Enter these two words correctly (with spaces) so we know you're not a robot (don't worry robots, we still love you). p.tiny <b>Last thing!</b> Enter these two words correctly (with spaces) so we know you're not a robot (don't worry robots, we still love you).
div style="background:#fff; width:100%; overflow:auto" div style="background:#fff; width:100%; overflow:auto"
== recaptcha_tag :challenge, ssl: true == recaptcha_tag :challenge, ssl: true