-
Notifications
You must be signed in to change notification settings - Fork 140
Rearrange, not change the abilities in Ability #997
Conversation
…st practices This is the work doc. Reset to here before continuing
…d abilities to meet specs.
@carpodaster Trying to unmess all things Ability from the previous PR. What do you think of this new setup? I have also made a start with the specs, but I don't want to throw too much stuf. LMK if you want to see them? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a valid approach 👍
I guess we can in the next step probably remove half of the special abilities anyways, since they're probably not used 😉
As for the specs: you're right, they look a bit ☠️ ... I'd personally just rewrite them from scratch then.
Do you want to add more changes to this PR or just merge this as a "first step"?
app/models/ability.rb
Outdated
can :read_email, User, hide_email: false # view helper | ||
can :read, Team | ||
can :read, Project | ||
can :read, :feed_entry |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First off: forgive my questions, I'm not familiar with CanCan - I'm always using Pundit...
What does this syntax here mean? :feed_entry
is a symbol, not a class / model... ❓
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I <3 your questions.
It is an :kind of an Activity, and used in lib/feed/item. I left it in here for now, because I am not changing abilities until they are thoroughly tested 😇
Thanks @klappradla ! Let's not merge yet, I'll go rewrite the specs first. Here is a taste of what they are going to look like. So excited about this! Look how neat it is going to be! |
app/models/ability.rb
Outdated
can :create, Project if user.confirmed? | ||
|
||
# current_student | ||
can :crud, Conference if user.current_student? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would we need to add && user.confirmed?
to all the checks below to make sure they are only available to confirmed users?
I like this organization - it seems clearer to me. Mixing "confirmed" and "unconfirmed" with user roles seems a bit odd to me, though.
Update: ok, things make a bit more sense after I read the "Give permissions, don't take them away" link you posted. Doing things like return unless user.confirmed?
right before line 20 ensures that everything below is only for confirmed users. 🙌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow! That was 💥
Let me know if I can do anything else to make reviewing less hard. |
@@ -1,6 +1,6 @@ | |||
FactoryBot.define do | |||
factory :user, aliases: [:member] do | |||
github_handle { FFaker::InternetSE.user_name_variant_short } | |||
github_handle { FFaker::InternetSE.unique.user_name_variant_short } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fixes flickering specs because of failing uniqueness validation on gh handle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solid work 💪
I left some comments and suggestion here and there.
I did not read through the diff of the old abilities specs because I though we're probably going to get rid of it on the long run anyways, right?
Huge ➕ on your work on the specs 👍
I'm not 100% sure if shared examples are the best approach here on the long run though 🤔
But that's only personal preference, no need to address this ✌️
@@ -1,11 +1,10 @@ | |||
# frozen_string_literal: true | |||
class MailingsController < ApplicationController | |||
|
|||
load_and_authorize_resource except: :index | |||
load_and_authorize_resource |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
app/models/ability.rb
Outdated
end | ||
|
||
# current_student | ||
if user.current_student? # TODO is this a valid check? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I remember this checks whether the user is a student in an accepted team for the current season.
So this may already be scoped too narrow here... At least
user.teams.none?
and creating teams in general does not make much sense then 🤔
spec/factories/users.rb
Outdated
@@ -84,5 +84,9 @@ | |||
create(:reviewer_role, user: user) | |||
end | |||
end | |||
|
|||
trait :unconfirmed do | |||
confirmed_at { nil } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for a block here.
spec/models/ability_spec.rb
Outdated
it_behaves_like "can not comment" | ||
it_behaves_like "has no access to other user's accounts" | ||
it_behaves_like "can not read role restricted or owner restricted pages" | ||
it "does not have an account" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this doing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a pro memory. If you have to ask, it is not working, I'll delete it.
spec/models/ability_spec.rb
Outdated
it_behaves_like "can see mailings list too" | ||
it_behaves_like "can read mailings sent to them" | ||
it_behaves_like "can comment now" # not implemented; false positives; needs work | ||
it { expect(subject).to be_able_to([:join, :create], Team) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cosmetic, but it's not necessary to write subject
when using the short hand syntax. You can simply write:
it { is_expected.to be_able_to %i(join create), Team }
Same for all further appearance 😉
spec/models/ability_spec.rb
Outdated
end | ||
end | ||
it 'cannot create a second team in current_season' do | ||
pending 'fails; it is tested in team_spec.rb:33, and that passes' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I fear you have to actually write the teams to the DB to have a test like this...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That ^ and we have this for confirmed users on line 27:
can [:join, :create], Team
By the way, instead of persisting the teams in the DB maybe you can get away with stubbing the user.teams
call.
spec/models/ability_spec.rb
Outdated
it { expect(ability).not_to be_able_to(:resend_confirmation_instruction, other_user) } | ||
before do | ||
allow(user).to receive(:supervisor?).and_return(true) | ||
allow(ability).to receive(:supervises?) { false } # Rspec complained: "stub default value first" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I sorta understand why RSpec does this 😉
I'd personally never use the receive(:thing).with
when defining a mock (with allow
). The matcher / syntax makes way more sense for expecting message calls in my opinion.
So within the test you'd have something like
expect(model).to receive(:thing?).with(other_thing).and_return(false)
☝️ testing the "contract" so to say.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to look up the difference, but I still don't understand what it is happening.
If I change the line to
expect(ability).to receive(:supervises?).with(other_user, user) { true }
it fails with received 0 times
Do you understand what I am missing here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sorry, I did not mean you should change it!
Expecting a method call only makes sense (and will only pass) within an example. So if your test want to check whether or not the specific message is passed to this object but you don't care about the inner logic and want to stub it, it may look like this:
it 'is able to do something' do
expect(ability).to receive(:supervise?).with(other_user, user).and_return(true)
expect(ability).to be_able_to(:read, :users_info, other_user)
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OOOOH... and you actually said that: "within an example". Sorry.
NOW I understand the fail message "provide a stub first" 💃
spec/models/ability_spec.rb
Outdated
let(:user){ nil } | ||
let(:other_user) { create(:user) } | ||
|
||
describe "Guest User" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personal preference and only a suggestion, bud I'd make all of these role-specific describes
a context
, such as
context 'when the user is a guest' do
# details for unrestricted | ||
shared_examples 'can view public pages' do | ||
PUBLIC_INDEX_PAGES.each do |page| | ||
it { expect(subject).to be_able_to(:read, page) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here: you can ditch the subject
when using this syntax:
it { is_expected.to be_able_to(:read, page) }
Same for the rest of this file of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hate that syntax with a passion. 😅 There is no performance or functional difference, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I tbh. don't know, but probably not 🤔
I personally think the syntax reads way more natural than explictly saying "subject". Take for instance
describe thing - it is expected to be such a thing
Since I haven't yet been on a project where linters would allow you syntax 🚔 🚒 - I'm probably also just used to the implicit one 😂
@@ -0,0 +1,95 @@ | |||
# collections of shared examples |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using shared examples is definitely a valid approach ✔️
I'm personally not a huge fan of using them in this amount and spread across multiple files though.
Well, comes down to personal preference, but I'm just quickly dumping my brain here: From my experience, they are usually only maintainable if being used only a few times and defined in the same file - it's the only way how a human brain can still keep track of the implicit contracts between the files. Plus: updating the feature and respectively the specs behind it gets super complicated, especially when nesting them like this.
They don't even run faster 😉
Alternative approaches here could for instance be:
- split the abilities specs in one file per role (prefer that tbh)
- test based on the requested entity rather then the role (and maybe again split into muliple files)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I totally agree that is not an ideal solution. It crossed my mind to split the abilities themselves in different files, but your suggestion to split only the specs in different files sounds better. I'd rather save that for a next step, if that is okay.
Moreover, the shared examples are mostly dealing with 'authentication stages', not roles. If everything works out with the Big Overhaul, we don't have to have that in Ability anymore... 🍀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure!
As said, your approach is totally valid 👍
I'd personally have done it differently and just wanted to explain this in case you were actually looking for alternative approaches or did not think about certain gotchas that come with it.
So all good to stay with this.
app/models/ability.rb
Outdated
can :resend_confirmation_instruction, User, id: user.id | ||
can :read_email, User, hide_email: false # view helper # delete? only used once | ||
|
||
return unless user.confirmed? && signed_in?(user) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the signed_in?(user)
is redundant - you've already returned for signed out users above.
# logged in, unconfirmed: | ||
shared_examples "same as guest user" do | ||
it_behaves_like "can view public pages" | ||
it_behaves_like "can not modify things on public pages" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small stylistic suggestion: maybe use cannot
instead of can not
? I think it makes it easier to parse out negatives and matches CanCanCan's cannot
.
(Same applies to the rest of the specs)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm They are not always used the same as cancancan's cannot
's. That made it more confusing, imo, so I left them as is.
- remove redundant conditional
- remove redundant things
…o be able to create a new team -PM rspec green
…ot read a user's email address when it's marked as hidden -PM rspec green
… desired behaviour - Added new specs to described the intended behaviour whether a supervisor should or should not read hidden email addresses. - PM rspec green
@klappradla @pgaspar Thanks so much for your reviews! 🙇 Here is the next step.
Awaiting your reactions! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@F3PiX Thank you very much for your great work here! I love the new clarity and specs.
As far as I understood, we need to continue with the cleanups before merging this. Is that correct? Or can we merge and carry on from there?
Hi Ramon, we merge this one first, as soon as it is approved. And carry on from there. |
On hold! I found a way to remove one level of nesting in the shared_examples (inspired by Max' request tests) and I am adding issues for the TODO's. |
…ut solving the issue :-) - Unchanged: a student cannot create a second team in one season, and can create a team in next season - Fixed: a student in a team in current season doesn't get the '+ new team' button - Fixed: a student in a team in current season has no access to the new team form - Driving by: use the new rails 5 redirect-er to redirect back to the referer or fall back to root. (So that the forbidden access message appears on the page where the user tried something forbidden.) - Update projects_spec for new redirect - Update teams_controller_spec that failed because a student can't create a second team in the same season
…n of testing role abilities.
before do | ||
user.roles.create(name: 'student', team: team) | ||
end | ||
|
||
describe "GET index" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that the new ability rule prevents students from creating a second team, the specs can't assign two teams either. Moved the one global before
into the contexts.
after { Timecop.return } | ||
|
||
it "can view the current season's accepted and selected projects" do | ||
# project not visible on page. why? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to #1002?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Could be...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AHA! I had to use a in_current_season
scope 🌞
@@ -33,7 +33,7 @@ def after_sign_in_path_for(user) | |||
end | |||
|
|||
rescue_from CanCan::AccessDenied do |exception| | |||
redirect_to root_url, alert: exception.message | |||
redirect_back(fallback_location: root_path, alert: exception.message) | |||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🏎 The 'not authorised' method will now show up on the page where the user tried to do something forbidden. We couldn't redirect :back
before, because it would mess up if there wasn't a referer present.
|
Thanks all, for your reviews!! 💟 |
Update:
Rearranged, not changed the abilities in Ability.
Following this principles for Defining Abilities:
The reason: it is easier to follow what a user can or cannot do. Less error prone and easier to reason about.
It is also 1000 times easier to write and read specs.
Follow up:
Questions about current abilities/implementation: