fingerrails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/README.rdoc +3 -0
- data/Rakefile +28 -0
- data/VERSION +1 -0
- data/bin/fingerrails +9 -0
- data/fingerrails.gemspec +90 -0
- data/fingertips.rb +181 -0
- data/templates/.kick +22 -0
- data/templates/Rakefile +23 -0
- data/templates/app/controllers/application_controller.rb +51 -0
- data/templates/app/controllers/members_controller.rb +32 -0
- data/templates/app/controllers/passwords_controller.rb +46 -0
- data/templates/app/controllers/sessions_controller.rb +26 -0
- data/templates/app/helpers/application_helper.rb +20 -0
- data/templates/app/models/mailer.rb +8 -0
- data/templates/app/models/member.rb +10 -0
- data/templates/app/models/member/authentication.rb +44 -0
- data/templates/app/views/layouts/_application_javascript_includes.html.erb +3 -0
- data/templates/app/views/layouts/_head.html.erb +3 -0
- data/templates/app/views/layouts/application.html.erb +25 -0
- data/templates/app/views/mailer/reset_password_message.erb +8 -0
- data/templates/app/views/members/edit.html.erb +21 -0
- data/templates/app/views/members/new.html.erb +26 -0
- data/templates/app/views/members/show.html.erb +3 -0
- data/templates/app/views/passwords/edit.html.erb +22 -0
- data/templates/app/views/passwords/new.html.erb +22 -0
- data/templates/app/views/passwords/reset.html.erb +9 -0
- data/templates/app/views/passwords/sent.html.erb +11 -0
- data/templates/app/views/sessions/_form.html.erb +22 -0
- data/templates/app/views/sessions/_status.html.erb +9 -0
- data/templates/app/views/sessions/new.html.erb +5 -0
- data/templates/config/database.yml +18 -0
- data/templates/lib/active_record_ext.rb +26 -0
- data/templates/lib/token.rb +9 -0
- data/templates/public/403.html +29 -0
- data/templates/public/javascripts/ready.js +9 -0
- data/templates/public/stylesheets/default.css +143 -0
- data/templates/public/stylesheets/reset.css +52 -0
- data/templates/test/ext/authentication.rb +24 -0
- data/templates/test/ext/file_fixtures.rb +8 -0
- data/templates/test/ext/time.rb +8 -0
- data/templates/test/fixtures/members.yml +8 -0
- data/templates/test/functional/application_controller_test.rb +104 -0
- data/templates/test/functional/members_controller_test.rb +71 -0
- data/templates/test/functional/passwords_controller_test.rb +95 -0
- data/templates/test/functional/sessions_controller_test.rb +68 -0
- data/templates/test/lib/active_record_ext_test.rb +13 -0
- data/templates/test/lib/token_test.rb +17 -0
- data/templates/test/test_helper.rb +32 -0
- data/templates/test/unit/helpers/application_helper_test.rb +44 -0
- data/templates/test/unit/mailer_test.rb +13 -0
- data/templates/test/unit/member/authentication_test.rb +73 -0
- data/templates/test/unit/member_test.rb +32 -0
- metadata +108 -0
@@ -0,0 +1,9 @@
|
|
1
|
+
// Let the javascript libs start their behaviour. These are all included at the
|
2
|
+
// end of the document so the browser will first render the document before
|
3
|
+
// requesting the javascripts.
|
4
|
+
|
5
|
+
// Event.observe(document, 'dom:loaded', function() {
|
6
|
+
// });
|
7
|
+
|
8
|
+
// Event.observe(window, 'load', function() {
|
9
|
+
// });
|
@@ -0,0 +1,143 @@
|
|
1
|
+
/* @group Typography */
|
2
|
+
|
3
|
+
html,
|
4
|
+
input
|
5
|
+
{
|
6
|
+
font: 14px/20px "Lucida Grande", Lucida, "Lucida Sans Unicode", Verdana, sans-serif;
|
7
|
+
}
|
8
|
+
|
9
|
+
h1
|
10
|
+
{
|
11
|
+
font-size: 200%;
|
12
|
+
}
|
13
|
+
h2
|
14
|
+
{
|
15
|
+
font-size: 150%;
|
16
|
+
margin-bottom: 24px;
|
17
|
+
}
|
18
|
+
|
19
|
+
/* @end */
|
20
|
+
|
21
|
+
/* @group Basics */
|
22
|
+
|
23
|
+
a,
|
24
|
+
a:visited
|
25
|
+
{
|
26
|
+
color: #689051;
|
27
|
+
}
|
28
|
+
|
29
|
+
a:hover,
|
30
|
+
a:active
|
31
|
+
{
|
32
|
+
color: #84b567;
|
33
|
+
}
|
34
|
+
|
35
|
+
/* @end */
|
36
|
+
|
37
|
+
/* @group Layout */
|
38
|
+
|
39
|
+
a#logo
|
40
|
+
{
|
41
|
+
display: block;
|
42
|
+
padding: 6px 12px;
|
43
|
+
color: #999;
|
44
|
+
text-decoration: none;
|
45
|
+
}
|
46
|
+
|
47
|
+
p#member
|
48
|
+
{
|
49
|
+
position: absolute;
|
50
|
+
top: 3px;
|
51
|
+
right: 12px;
|
52
|
+
}
|
53
|
+
p#member a
|
54
|
+
{
|
55
|
+
text-decoration: none;
|
56
|
+
font-size: 10px;
|
57
|
+
}
|
58
|
+
p#member a+a
|
59
|
+
{
|
60
|
+
margin-left: 6px;
|
61
|
+
}
|
62
|
+
|
63
|
+
ul#navigation
|
64
|
+
{
|
65
|
+
background-color: #bdff94;
|
66
|
+
padding: 6px 12px;
|
67
|
+
margin-bottom: 12px;
|
68
|
+
border-top: 1px solid #bfff97;
|
69
|
+
border-bottom: 1px solid #a0da7d;
|
70
|
+
}
|
71
|
+
ul#navigation li
|
72
|
+
{
|
73
|
+
display: inline;
|
74
|
+
}
|
75
|
+
ul#navigation a,
|
76
|
+
ul#navigation a:visited
|
77
|
+
{
|
78
|
+
text-decoration: none;
|
79
|
+
color: #222;
|
80
|
+
}
|
81
|
+
ul#navigation a:hover,
|
82
|
+
ul#navigation a:active
|
83
|
+
{
|
84
|
+
color: #7d9b1d;
|
85
|
+
}
|
86
|
+
ul#navigation li + li
|
87
|
+
{
|
88
|
+
margin-left: 6px;
|
89
|
+
}
|
90
|
+
|
91
|
+
div#main
|
92
|
+
{
|
93
|
+
padding: 12px;
|
94
|
+
}
|
95
|
+
|
96
|
+
/* @end */
|
97
|
+
|
98
|
+
/* @group Forms */
|
99
|
+
|
100
|
+
div.field
|
101
|
+
{
|
102
|
+
margin: 12px 0px;
|
103
|
+
}
|
104
|
+
label,
|
105
|
+
div.label
|
106
|
+
{
|
107
|
+
color: #666;
|
108
|
+
}
|
109
|
+
div.label a
|
110
|
+
{
|
111
|
+
color: #999;
|
112
|
+
font-size: 80%;
|
113
|
+
text-decoration: none;
|
114
|
+
}
|
115
|
+
div.label a:hover
|
116
|
+
{
|
117
|
+
text-decoration: underline;
|
118
|
+
}
|
119
|
+
div.inline label
|
120
|
+
{
|
121
|
+
font-size: 90%;
|
122
|
+
}
|
123
|
+
|
124
|
+
input[type="text"],
|
125
|
+
input[type="password"]
|
126
|
+
{
|
127
|
+
color: #222;
|
128
|
+
}
|
129
|
+
|
130
|
+
a.cancel
|
131
|
+
{
|
132
|
+
text-decoration: none;
|
133
|
+
color: #999;
|
134
|
+
font-size: 80%;
|
135
|
+
margin-left: 3px;
|
136
|
+
}
|
137
|
+
a.cancel:hover
|
138
|
+
{
|
139
|
+
text-decoration: underline;
|
140
|
+
}
|
141
|
+
|
142
|
+
/* @end */
|
143
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
/* Reset, based on http://meyerweb.com/eric/tools/css/reset/ */
|
2
|
+
|
3
|
+
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, button, legend, table, caption, tbody, tfoot, thead, tr, th, td
|
4
|
+
{
|
5
|
+
margin: 0;
|
6
|
+
padding: 0;
|
7
|
+
border: 0;
|
8
|
+
outline: 0;
|
9
|
+
font-size: 100%;
|
10
|
+
vertical-align: baseline;
|
11
|
+
background: transparent;
|
12
|
+
}
|
13
|
+
body
|
14
|
+
{
|
15
|
+
line-height: 1;
|
16
|
+
}
|
17
|
+
ol, ul
|
18
|
+
{
|
19
|
+
list-style: none;
|
20
|
+
}
|
21
|
+
blockquote, q
|
22
|
+
{
|
23
|
+
quotes: none;
|
24
|
+
}
|
25
|
+
blockquote:before, blockquote:after, q:before, q:after
|
26
|
+
{
|
27
|
+
content: '';
|
28
|
+
content: none;
|
29
|
+
}
|
30
|
+
:focus /* remember to define focus styles! */
|
31
|
+
{
|
32
|
+
outline: 0;
|
33
|
+
}
|
34
|
+
ins /* remember to highlight inserts somehow! */
|
35
|
+
{
|
36
|
+
text-decoration: none;
|
37
|
+
}
|
38
|
+
del
|
39
|
+
{
|
40
|
+
text-decoration: line-through;
|
41
|
+
}
|
42
|
+
table /* tables still need 'cellspacing="0"' in the markup */
|
43
|
+
{
|
44
|
+
border-collapse: collapse;
|
45
|
+
border-spacing: 0;
|
46
|
+
}
|
47
|
+
hr
|
48
|
+
{
|
49
|
+
height: 1px;
|
50
|
+
background-color: #000;
|
51
|
+
border: none;
|
52
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module TestHelpers
|
2
|
+
module Authentication
|
3
|
+
def login(member, password='secret')
|
4
|
+
@authenticated = member
|
5
|
+
request.session[:member_id] = @authenticated.id
|
6
|
+
end
|
7
|
+
|
8
|
+
def logout
|
9
|
+
request.session.delete(:member_id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def authenticated?
|
13
|
+
!request.session[:member_id].blank?
|
14
|
+
end
|
15
|
+
|
16
|
+
def access_denied?
|
17
|
+
response.status.to_i == 403
|
18
|
+
end
|
19
|
+
|
20
|
+
def login_required?
|
21
|
+
response.header['Location'] == new_session_url
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module TestHelpers
|
2
|
+
module FileFixtures
|
3
|
+
def fixture_file_upload(path, mime_type = nil, binary = false)
|
4
|
+
fixture_path = ActionController::TestCase.send(:fixture_path) if ActionController::TestCase.respond_to?(:fixture_path)
|
5
|
+
ActionController::TestUploadedFile.new("#{fixture_path}#{path}", mime_type, binary)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class TestApplicationsController < ApplicationController
|
4
|
+
allow_access :admin, :only => :private_action
|
5
|
+
allow_access :all, :except => :private_action
|
6
|
+
|
7
|
+
def private_action
|
8
|
+
render :nothing => true
|
9
|
+
end
|
10
|
+
|
11
|
+
def private_action_with_flash
|
12
|
+
flash[:notice] = 'Message'
|
13
|
+
render :nothing => true
|
14
|
+
end
|
15
|
+
|
16
|
+
def public_action
|
17
|
+
render :nothing => true
|
18
|
+
end
|
19
|
+
|
20
|
+
def action_with_layout
|
21
|
+
render :inline => '', :layout => 'application'
|
22
|
+
end
|
23
|
+
|
24
|
+
def action_with_respond_block
|
25
|
+
respond_to do |format|
|
26
|
+
format.html { render :text => 'HTML' }
|
27
|
+
format.jpg { render :text => 'JPG'}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
ActionController::Routing::Routes.draw do |map|
|
33
|
+
map.resource :test_application, :member => {
|
34
|
+
:private_action => :get,
|
35
|
+
:private_action_with_flash => :get,
|
36
|
+
:public_action => :get,
|
37
|
+
:action_with_layout => :get,
|
38
|
+
:action_with_respond_block => :get
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
describe TestApplicationsController do
|
43
|
+
it "should find the currently logged in member" do
|
44
|
+
login members(:adrian)
|
45
|
+
get :public_action
|
46
|
+
assigns(:authenticated).should == members(:adrian)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should not find currently logged in member when no-one is logged in" do
|
50
|
+
get :public_action
|
51
|
+
assigns(:authenticated).should.be.blank
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should log a member in" do
|
55
|
+
should.not.be.authenticated
|
56
|
+
@controller.send(:login, members(:adrian))
|
57
|
+
should.be.authenticated
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should log a member out" do
|
61
|
+
@controller.send(:login, members(:adrian))
|
62
|
+
should.be.authenticated
|
63
|
+
@controller.send(:logout)
|
64
|
+
should.not.be.authenticated
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should respond with HTML before JPG" do
|
68
|
+
request.env['HTTP_ACCEPT'] = "*/*"
|
69
|
+
get :action_with_respond_block
|
70
|
+
response.body.should == 'HTML'
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should respond with HTML before JPG, even with IE" do
|
74
|
+
request.env["HTTP_USER_AGENT"] = 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Win64; x64)'
|
75
|
+
request.env['HTTP_ACCEPT'] = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/x-shockwave-flash, application/x-silverlight, */*"
|
76
|
+
get :action_with_respond_block
|
77
|
+
response.body.should == 'HTML'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe TestApplicationsController, "when dealing with pages that need authorization" do
|
82
|
+
it "should allow the user to authorize" do
|
83
|
+
get :private_action
|
84
|
+
should.redirect_to new_session_url
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should keep the flash around when access was refused" do
|
88
|
+
get :private_action_with_flash
|
89
|
+
flash[:notice].should == 'Message'
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should have stored the url to return the user to after authentication" do
|
93
|
+
get :private_action
|
94
|
+
controller.after_authentication[:redirect_to].should == controller.url_for(:action => :private_action)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should show a forbidden page when access was refused" do
|
98
|
+
controller.stubs(:find_authenticated)
|
99
|
+
controller.send(:instance_variable_set, "@authenticated", stub(:id => 1, :role => 'member'))
|
100
|
+
|
101
|
+
get :private_action
|
102
|
+
status.should.be :forbidden
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "On the", MembersController, "a visitor" do
|
4
|
+
it "should see a form for a new member" do
|
5
|
+
get :new
|
6
|
+
status.should.be :ok
|
7
|
+
template.should.be 'members/new'
|
8
|
+
assert_select 'form'
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should create a new member" do
|
12
|
+
lambda {
|
13
|
+
post :create, :member => valid_params
|
14
|
+
}.should.differ('Member.count', +1)
|
15
|
+
assigns(:member).email.should == valid_params[:email]
|
16
|
+
assigns(:member).hashed_password.should == Member.hash_password(valid_params[:password])
|
17
|
+
should.redirect_to root_url
|
18
|
+
should.be.authenticated
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should show validation errors after a failed create" do
|
22
|
+
post :create, :member => valid_params.merge(:email => '')
|
23
|
+
status.should.be :ok
|
24
|
+
template.should.be 'members/new'
|
25
|
+
assert_select 'div.errorExplanation'
|
26
|
+
assert_select 'form'
|
27
|
+
should.not.be.authenticated
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should see a profile" do
|
31
|
+
get :show, :id => members(:adrian)
|
32
|
+
assigns(:member).should == members(:adrian)
|
33
|
+
status.should.be :success
|
34
|
+
template.should.be 'members/show'
|
35
|
+
end
|
36
|
+
|
37
|
+
should.require_login.get :edit, :id => members(:adrian)
|
38
|
+
should.require_login.put :update, :id => members(:adrian)
|
39
|
+
should.require_login.delete :destroy, :id => members(:adrian)
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def valid_params
|
44
|
+
{ :email => 'jurgen@example.com', :password => 'so secret', :verify_password => 'so secret' }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "On the", MembersController, "a member" do
|
49
|
+
before do
|
50
|
+
login members(:adrian)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should see an edit form" do
|
54
|
+
get :edit, :id => @authenticated.to_param
|
55
|
+
|
56
|
+
assert_select 'form'
|
57
|
+
assigns(:member).should == @authenticated
|
58
|
+
status.should.be :success
|
59
|
+
template.should.be 'members/edit'
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should be able to update his profile" do
|
63
|
+
put :update, :id => @authenticated.to_param, :member => { :email => 'sir.adrian@example.com' }
|
64
|
+
|
65
|
+
@authenticated.reload.email.should == 'sir.adrian@example.com'
|
66
|
+
should.redirect_to member_url(@authenticated)
|
67
|
+
end
|
68
|
+
|
69
|
+
should.disallow.put :update, :id => members(:kelly)
|
70
|
+
should.disallow.delete :destroy, :id => members(:kelly)
|
71
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "On the", PasswordsController, "a visitor" do
|
4
|
+
before do
|
5
|
+
@member = members(:adrian)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should see a new form to reset his password" do
|
9
|
+
get :new
|
10
|
+
status.should.be :success
|
11
|
+
template.should.be 'passwords/new'
|
12
|
+
assert_select 'form'
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should generate a password reset token and send it via email" do
|
16
|
+
@member.update_attribute(:reset_password_token, nil)
|
17
|
+
|
18
|
+
assert_emails(1) do
|
19
|
+
post :create, :email => @member.email
|
20
|
+
end
|
21
|
+
|
22
|
+
assigns(:member).should == @member
|
23
|
+
@member.reload.reset_password_token.should.not.be.blank
|
24
|
+
|
25
|
+
email = ActionMailer::Base.deliveries.last
|
26
|
+
email.body.should.include edit_password_url(:id => @member.reset_password_token)
|
27
|
+
|
28
|
+
status.should.be :success
|
29
|
+
template.should.be 'passwords/sent'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should see an error if no member can be found for a given email address" do
|
33
|
+
assert_emails(0) do
|
34
|
+
post :create, :email => "joe.the.plumber@example.com"
|
35
|
+
end
|
36
|
+
|
37
|
+
status.should.be :ok
|
38
|
+
template.should.be 'passwords/new'
|
39
|
+
flash[:error].should.not.be.blank
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should see a warning if an SMTP error occurred" do
|
43
|
+
Mailer.stubs(:deliver_reset_password_message).raises Net::SMTPFatalError
|
44
|
+
post :create, :email => @member.email
|
45
|
+
|
46
|
+
status.should.be :ok
|
47
|
+
template.should.be 'passwords/new'
|
48
|
+
flash[:error].should.not.be.blank
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should see an edit form for his password" do
|
52
|
+
@member.generate_reset_password_token!
|
53
|
+
get :edit, :id => @member.reset_password_token
|
54
|
+
|
55
|
+
status.should.be :success
|
56
|
+
assigns(:member).should == @member
|
57
|
+
template.should.be 'passwords/edit'
|
58
|
+
assert_select 'form'
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should not see an edit form if no member is found for the given token" do
|
62
|
+
get :edit, :id => "doesnotexist"
|
63
|
+
|
64
|
+
status.should.be :not_found
|
65
|
+
assigns(:member).should.be nil
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should be able to update his password" do
|
69
|
+
@member.generate_reset_password_token!
|
70
|
+
put :update, :id => @member.reset_password_token, :password => "newpass"
|
71
|
+
|
72
|
+
Member.authenticate(:email => @member.email, :password => "newpass").should == @member
|
73
|
+
status.should.be :success
|
74
|
+
template.should.be 'passwords/reset'
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should see an error if the password is empty" do
|
78
|
+
@member.generate_reset_password_token!
|
79
|
+
before = @member.hashed_password
|
80
|
+
|
81
|
+
put :update, :id => @member.reset_password_token, :password => ""
|
82
|
+
|
83
|
+
@member.reload.hashed_password.should == before
|
84
|
+
status.should.be :success
|
85
|
+
template.should.be 'passwords/edit'
|
86
|
+
assert_select "div.errorExplanation", :text => "The password can’t be blank."
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should not be allowed to update a password with an incorrect token" do
|
90
|
+
put :update, :id => "doesnotexist", :password => "newpass"
|
91
|
+
|
92
|
+
status.should.be :not_found
|
93
|
+
assigns(:member).should.be nil
|
94
|
+
end
|
95
|
+
end
|