Skip to content
This repository has been archived by the owner on Jun 27, 2023. It is now read-only.

Rearrange, not change the abilities in Ability #997

Merged
merged 20 commits into from
May 23, 2018
Merged

Rearrange, not change the abilities in Ability #997

merged 20 commits into from
May 23, 2018

Conversation

emcoding
Copy link
Member

@emcoding emcoding commented May 15, 2018

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:

  • In the console, I assigned my team_less account a student_role. I added a conf. Is that even possible IRL? When hitting Conferences#create, it redirects to -path. => UrlGenerationError. Conference not created. Create issue or irrelevant?

Questions about current abilities/implementation:

  • A student can destroy their own team, which make sense. But what about an accepted team, mid-season?
  • A supervisor can read some extra details of the users; AFAIK it concerns email_addresses and team's blogpost_info. That makes sense for their own team, but not for users on other teams, right? It needs further investigation. -> issue Fix cancancan implementation of Supervisor ability #1001
  • Note: Some 'ability-ish' feature are managed by methods in application_helper.rb (:can_see_private_info, :can_only_review_private-info.).

…st practices

This is the work doc.
Reset to here before continuing
@emcoding
Copy link
Member Author

@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?

Copy link
Member

@klappradla klappradla left a 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"?

can :read_email, User, hide_email: false # view helper
can :read, Team
can :read, Project
can :read, :feed_entry
Copy link
Member

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... ❓

Copy link
Member Author

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 😇

@emcoding
Copy link
Member Author

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!

can :create, Project if user.confirmed?

# current_student
can :crud, Conference if user.current_student?
Copy link
Collaborator

@pgaspar pgaspar May 16, 2018

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. 🙌

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, @pgaspar , instead of all the confirmed? checks, we will have one return statement. 😍
I agree it would be even neater to have no authentication check in here at all, but we'll probably going to need that until we went through everything in #991. 🍀
Just a few steps away 🤣

@emcoding
Copy link
Member Author

emcoding commented May 17, 2018

Wow! That was 💥
I TRIED to make this as easy for you reviewers as I could. 😇 :

  • Commit f6da595 is a clean file with some questions. The last commit before that has all kinds of annotations. They explain some of the cancancan quirks, so that should answer some of the questions you may have.
  • There is more work to do, but I'd thought this makes for a reasonable pit stop.
  • Note that the abilities are not complete; I didn't add things, only rearranged what we already had. And all of those are covered by tests now. 🎉
    Questions about the current abilities, and if they should be changed, are listed in the PR description ^.
  • There is some noise in the specs. I'd like to stub things, so that we are only testing the ability. Help is much appreciated!

Let me know if I can do anything else to make reviewing less hard.
@klappradla @carpodaster @pgaspar @ramonh

@emcoding emcoding changed the title WIP Rearrange, not change the abilities in Ability Rearrange, not change the abilities in Ability May 17, 2018
@@ -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 }
Copy link
Member Author

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.

Copy link
Member

@klappradla klappradla left a 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

end

# current_student
if user.current_student? # TODO is this a valid check?
Copy link
Member

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 🤔

@@ -84,5 +84,9 @@
create(:reviewer_role, user: user)
end
end

trait :unconfirmed do
confirmed_at { nil }
Copy link
Member

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.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this doing?

Copy link
Member Author

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.

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) }
Copy link
Member

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 😉

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'
Copy link
Member

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...

Copy link
Collaborator

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.

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"
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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

Copy link
Member Author

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" 💃

let(:user){ nil }
let(:other_user) { create(:user) }

describe "Guest User" do
Copy link
Member

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) }
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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
Copy link
Member

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)

Copy link
Member Author

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... 🍀

Copy link
Member

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.

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)
Copy link
Collaborator

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"
Copy link
Collaborator

@pgaspar pgaspar May 18, 2018

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)

Copy link
Member Author

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
@emcoding
Copy link
Member Author

@klappradla @pgaspar Thanks so much for your reviews! 🙇 Here is the next step.

  • The problems with the specs turned out to be covering bigger problems with the implementation of cancancan in other places. With the Main Goal in mind - the Registration Overhaul - I reverted some of the changes to the previous state; they are working, and not needed for fixing the registration process. I did add specs for the desired behaviour though.
  • With splitting the ability_spec file in separate spec files per role, it looks promising, but I don't know what to do now with the shared_examples. Should I just copy paste the examples into each file, and accept all the duplication? Or is there a better way?
  • Bonus: the first registrations' feature test, for the guest user.

Awaiting your reactions!

Copy link
Member

@hola-soy-milk hola-soy-milk left a 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?

@emcoding
Copy link
Member Author

Hi Ramon, we merge this one first, as soon as it is approved. And carry on from there.

@emcoding
Copy link
Member Author

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
before do
user.roles.create(name: 'student', team: team)
end

describe "GET index" do
Copy link
Member Author

@emcoding emcoding May 21, 2018

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?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to #1002?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Could be...

Copy link
Member Author

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
Copy link
Member Author

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.

@emcoding
Copy link
Member Author

  • Removed the nesting in the shared_examples
  • The common examples of confirmed users are not repeated in every confirmed role anymore.
  • Solved issue Don't show the Team new form if a user is not allowed to create one.  #1003, because it was easier than to leave it alone.
  • Added the new Rails 5 redirect_back method, so that unauthorised users are getting the alert on the page where the forbidden action happened.

@klappradla @pgaspar

@emcoding emcoding merged commit 7acbe86 into master May 23, 2018
@emcoding
Copy link
Member Author

Thanks all, for your reviews!! 💟

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants