Compare commits

...

7 Commits

Author SHA1 Message Date
022094ce51
Add feature spec for whole signup process
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2020-12-03 14:50:02 +01:00
2a2b0a90dc Validate email address properly 2020-12-03 14:49:37 +01:00
e44535daee Don't use deprecated method 2020-12-03 14:49:16 +01:00
c8ccb418b2 Add indexes for invitations table 2020-12-03 14:49:02 +01:00
a792d66c90 Show unused invitations list 2020-12-03 14:48:43 +01:00
f7e48ad3a6 Accept non-existing terms
Legal how does it work
2020-12-03 14:47:57 +01:00
8a7d809b92
Add scopes for invitations 2020-12-03 14:04:58 +01:00
12 changed files with 185 additions and 38 deletions

View File

@ -25,4 +25,11 @@ form {
.actions {
margin-top: 2rem;
}
.accept-terms {
margin-top: 2rem;
font-size: 0.85rem;
line-height: 1.5em;
color: #888;
}
}

View File

@ -6,7 +6,8 @@ class InvitationsController < ApplicationController
# GET /invitations
def index
@invitations = current_user.invitations
@invitations_unused = current_user.invitations.unused
@invitations_used = current_user.invitations.used
end
# GET /invitations/a-random-invitation-token

View File

@ -104,7 +104,6 @@ class SignupController < ApplicationController
password: @user.password
)
@invitation.update_attributes invited_user_id: @user.id,
used_at: DateTime.now
@invitation.update! invited_user_id: @user.id, used_at: DateTime.now
end
end

View File

@ -0,0 +1,15 @@
require 'mail'
module EmailValidatable
extend ActiveSupport::Concern
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
begin
a = Mail::Address.new(value)
rescue Mail::Field::ParseError
record.errors[attribute] << (options[:message] || "is not a valid address")
end
end
end
end

View File

@ -2,10 +2,16 @@ class Invitation < ApplicationRecord
# Relations
belongs_to :user
# Validations
validates_presence_of :user
# Hooks
before_create :generate_token
# Scopes
scope :unused, -> { where(used_at: nil) }
scope :used, -> { where.not(used_at: nil) }
private
def generate_token

View File

@ -1,10 +1,13 @@
class User < ApplicationRecord
include EmailValidatable
# Relations
has_many :invitations, dependent: :destroy
validates_uniqueness_of :cn
validates_uniqueness_of :email
validates_length_of :cn, :minimum => 3
validates_uniqueness_of :email
validates :email, email: true
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable

View File

@ -1,23 +1,46 @@
<section>
<h2>Invitations</h2>
<% if @invitations_unused.any? %>
<table>
<thead>
<tr>
<th>URL</th>
</tr>
</thead>
<tbody>
<% @invitations_unused.each do |invitation| %>
<tr>
<td><%= invitation_url(invitation.token) %></td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>
You do not have any invitations to give away yet. All good
things come in time.
</p>
<% end %>
</section>
<% if @invitations_used.any? %>
<h3>Accepted Invitations</h3>
<table>
<thead>
<tr>
<th>Token</th>
<th>Created at</th>
<th colspan="3"></th>
<th>URL</th>
<th>Used at</th>
<th>Invited user</th>
</tr>
</thead>
<tbody>
<% @invitations.each do |invitation| %>
<% @invitations_used.each do |invitation| %>
<tr>
<td><%= invitation.token %></td>
<td><%= invitation.created_at %></td>
<td><%= link_to 'Delete', invitation, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<td><%= invitation_url(invitation.token) %></td>
<td><%= invitation.used_at %></td>
<td><%= User.find(invitation.invited_user_id).address %></td>
</tr>
<% end %>
</tbody>
</table>
</section>
<% end %>

View File

@ -48,8 +48,14 @@
<p class="error-msg">Password <%= @validation_error %></p>
<% end %>
</div>
<p class="accept-terms">
<small>
By clicking the button below, you accept our future Terms of Service
and Privacy Policy. Don't worry, they will be excellent!
</small>
</p>
<div class="actions">
<p><%= f.submit "Continue" %></p>
<p><%= f.submit "Create account" %></p>
</div>
<% end %>
<% end %>

View File

@ -8,5 +8,7 @@ class CreateInvitations < ActiveRecord::Migration[6.0]
t.timestamps
end
add_index :invitations, :user_id
add_index :invitations, :invited_user_id
end
end

View File

@ -1,6 +1,5 @@
FactoryBot.define do
factory :invitation do
id { 1 }
token { "abcdef123456" }
user
end

View File

@ -3,37 +3,101 @@ require "rails_helper"
RSpec.describe "Signup", type: :feature do
let(:user) { create :user }
before do
@unused_invitation = Invitation.create(user: user)
@used_invitation = Invitation.create(user: user)
@used_invitation.update_attribute :used_at, DateTime.now - 1.day
end
describe "Invitation handling" do
before do
@unused_invitation = Invitation.create(user: user)
@used_invitation = Invitation.create(user: user)
@used_invitation.update_attribute :used_at, DateTime.now - 1.day
end
scenario "Follow link for non-existing invitation" do
visit invitation_url(id: "123")
scenario "Follow link for non-existing invitation" do
visit invitation_url(id: "123")
within ".flash-msg.alert" do
expect(page).to have_content("doesn't exist")
within ".flash-msg.alert" do
expect(page).to have_content("doesn't exist")
end
end
scenario "Follow link for used invitation" do
visit invitation_url(id: @used_invitation.token)
within ".flash-msg.alert" do
expect(page).to have_content("has already been used")
end
end
scenario "Follow link for unused invitation" do
visit invitation_url(id: @unused_invitation.token)
expect(current_url).to eq(signup_url)
expect(page).to have_content("Welcome")
end
end
scenario "Follow link for used invitation" do
visit invitation_url(id: @used_invitation.token)
within ".flash-msg.alert" do
expect(page).to have_content("has already been used")
describe "Signup steps" do
before do
@invitation = Invitation.create(user: user)
visit invitation_url(id: @invitation.token)
click_link "Get started"
end
end
scenario "Follow link for unused invitation" do
visit invitation_url(id: @unused_invitation.token)
scenario "Successful signup (happy path galore)" do
expect(page).to have_content("Choose a username")
expect(current_url).to eq(signup_url)
expect(page).to have_content("Welcome")
end
fill_in "user_cn", with: "tony"
click_button "Continue"
expect(page).to have_content("What's your email?")
scenario "Successful signup" do
visit invitation_url(id: @unused_invitation.token)
click_link "Get started"
fill_in "user_email", with: "tony@example.com"
click_button "Continue"
expect(page).to have_content("Choose a password")
expect(CreateAccount).to receive(:call)
.with(username: "tony", email: "tony@example.com", password: "a-valid-password")
.and_return(true)
fill_in "user_password", with: "a-valid-password"
click_button "Create account"
within ".flash-msg.notice" do
expect(page).to have_content("confirm your address")
end
expect(page).to have_content("close this window or tab now")
expect(User.last.confirmed_at).to be_nil
end
scenario "Validation errors" do
fill_in "user_cn", with: "t"
click_button "Continue"
expect(page).to have_content("Username is too short")
fill_in "user_cn", with: "jimmy"
click_button "Continue"
expect(page).to have_content("Username has already been taken")
fill_in "user_cn", with: "tony"
click_button "Continue"
fill_in "user_email", with: "tony@"
click_button "Continue"
expect(page).to have_content("Email is not a valid address")
fill_in "user_email", with: ""
click_button "Continue"
expect(page).to have_content("Email can't be blank")
fill_in "user_email", with: "tony@example.com"
click_button "Continue"
fill_in "user_password", with: "123456"
click_button "Create account"
expect(page).to have_content("Password is too short")
expect(CreateAccount).to receive(:call)
.with(username: "tony", email: "tony@example.com", password: "a-valid-password")
.and_return(true)
fill_in "user_password", with: "a-valid-password"
click_button "Create account"
within ".flash-msg.notice" do
expect(page).to have_content("confirm your address")
end
expect(User.last.cn).to eq("tony")
end
end
end

View File

@ -19,4 +19,26 @@ RSpec.describe Invitation, type: :model do
expect(token).not_to eq(invitation_2.token)
end
end
describe "scopes" do
before do
@unused_invitation = create :invitation, user: user
@used_invitation = create :invitation, user: user, used_at: DateTime.now
@used_invitation_2 = create :invitation, user: user, used_at: DateTime.now
end
describe "#unused" do
it "returns unused invitations" do
expect(Invitation.unused.count).to eq(1)
expect(Invitation.unused.first).to eq(@unused_invitation)
end
end
describe "#used" do
it "returns used invitations" do
expect(Invitation.used.count).to eq(2)
expect(Invitation.used.first).to eq(@used_invitation)
end
end
end
end