diff --git a/Gemfile b/Gemfile index 29c7b5d..ed815b6 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ gem 'appraisal' # Dependencies for dummy application gem 'sqlite3' -gem 'jsonapi-resources', github: 'cerebris/jsonapi-resources' +gem 'jsonapi-resources', '~> 0.8.0' gem 'pundit' gemspec diff --git a/lib/pundit/resource_controller.rb b/lib/pundit/resource_controller.rb index 39f0890..5f3c505 100644 --- a/lib/pundit/resource_controller.rb +++ b/lib/pundit/resource_controller.rb @@ -12,6 +12,7 @@ module ResourceController error = Pundit::NotAuthorizedError unless config.exception_class_whitelist.include? error config.exception_class_whitelist << error + config.use_relationship_reflection = true end end @@ -28,11 +29,12 @@ def enforce_policy_use def reject_forbidden_request(error) type = error.record.class.name.underscore.humanize(capitalize: false) + human_action = params[:action].humanize(capitalize: false) error = JSONAPI::Error.new( code: JSONAPI::FORBIDDEN, status: :forbidden, - title: "#{params[:action].capitalize} Forbidden", - detail: "You don't have permission to #{params[:action]} this #{type}.", + title: "#{human_action.titleize} Forbidden", + detail: "You don't have permission to #{human_action} this #{type}.", ) render json: { errors: [error] }, status: 403 diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb new file mode 100644 index 0000000..d6bbeb0 --- /dev/null +++ b/spec/controllers/application_controller_spec.rb @@ -0,0 +1,6 @@ +require "rails_helper" + +RSpec.describe ApplicationController, type: :controller do + it { is_expected.to be_a JSONAPI::ResourceController } + it { is_expected.to be_a Pundit::ResourceController } +end diff --git a/spec/controllers/create_spec.rb b/spec/controllers/create_spec.rb new file mode 100644 index 0000000..a2591c1 --- /dev/null +++ b/spec/controllers/create_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" + +RSpec.describe UsersController, type: :controller do + describe "#create" do + def do_request + post :create, params_hash(data: { type: :users }) + end + + context "but Pundit says no" do + before do + expect_any_instance_of(UserPolicy). + to receive(:create?).and_return(false) + end + + it "does not create a user" do + expect { do_request }.not_to change { User.count } + end + + it "responds with 403 Forbidden" do + do_request + expect(response).to have_http_status 403 + expect(body.dig(:errors, 0, :title)).to eq "Create Forbidden" + expect(body.dig(:errors, 0, :detail)).to eq <<-DESC.strip + You don't have permission to create this user. + DESC + end + end + + context "and Pundit says yes" do + before do + allow_any_instance_of(UserPolicy).to receive(:create?).and_return(true) + end + + it "creates a user" do + expect { do_request }.to change { User.count }.by 1 + end + + it "responds with 201 Created" do + do_request + expect(response).to have_http_status 201 + end + end + end +end diff --git a/spec/controllers/destroy_relationship_spec.rb b/spec/controllers/destroy_relationship_spec.rb new file mode 100644 index 0000000..4852607 --- /dev/null +++ b/spec/controllers/destroy_relationship_spec.rb @@ -0,0 +1,64 @@ +require "rails_helper" + +RSpec.describe PostsController, type: :controller do + describe "#destroy_relationship" do + let(:params) {{ + relationship: "user", + post_id: post_id, + data: { + type: "users", + id: user_id.to_s, + }, + }} + + def do_request + delete :destroy_relationship, params_hash(params) + end + + context "when the post does not exist" do + let(:user_id) { User.create!.id } + let(:post_id) { next_id Post } + before { do_request } + + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + end + + context "when the post exists" do + let!(:post) { Post.create! } + let(:post_id) { post.id } + + let!(:user) { User.create! } + let(:user_id) { user.id } + + context "but Pundit does not allow updating the post" do + before do + allow_any_instance_of(PostPolicy). + to receive(:update?).and_return(false) + do_request + end + + it "responds with 404 Forbidden" do + expect(response).to have_http_status 403 + end + end + + context "and Pundit allows updating the post" do + before do + allow_any_instance_of(PostPolicy). + to receive(:update?).and_return(true) + do_request + end + + it "responds with 204 No Content" do + expect(response).to have_http_status 204 + end + + it "diassociates post" do + expect(post.reload.user).to be_nil + end + end + end + end +end diff --git a/spec/controllers/destroy_spec.rb b/spec/controllers/destroy_spec.rb new file mode 100644 index 0000000..0d521f2 --- /dev/null +++ b/spec/controllers/destroy_spec.rb @@ -0,0 +1,91 @@ +require "rails_helper" + +RSpec.describe UsersController, type: :controller do + describe "#destroy" do + def do_request + delete :destroy, params_hash(id: id) + end + + context "when the user does not exist" do + let(:id) { next_id User } + + before { do_request } + + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + end + + context "when the user exists" do + let!(:user) { User.create! } + let(:id) { user.id } + + context "but Pundit says no" do + before do + allow_any_instance_of(UserPolicy). + to receive(:destroy?).and_return(false) + end + + context "when the user is not included in the scope" do + before do + allow_any_instance_of(UserPolicy::Scope). + to receive(:resolve).and_return(User.none) + do_request + end + + # Even though the resource exists, it is still correct to respond with + # 404 Not Found because the client shouldn't be able to determine + # whether a given resource exists. + # + # From RFC 2616: + # + # > If the server does not wish to make this information available to + # > the client, the status code 404 (Not Found) can be used instead. + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + + it "contains an error in the JSON response" do + expect(body[:errors].count).to eq 1 + end + end + + context "when the user is included in the scope" do + before do + allow_any_instance_of(UserPolicy::Scope). + to receive(:resolve).and_return(User.all) + do_request + end + + it "responds with 403 Forbidden" do + expect(response).to have_http_status 403 + end + + it "contains an error in the JSON response" do + expect(body[:errors].count).to eq 1 + expect(body.dig(:errors, 0, :title)).to eq "Destroy Forbidden" + expect(body.dig(:errors, 0, :detail)).to eq <<-DESC.strip + You don't have permission to destroy this user. + DESC + end + end + end + + context "and Pundit says yes" do + before do + allow_any_instance_of(UserPolicy). + to receive(:destroy?).and_return(true) + end + + it "destroys the user" do + expect { do_request }.to change { User.count }.by(-1) + end + + it "responds with 204 No Content" do + do_request + expect(response).to have_http_status 204 + end + end + end + end +end diff --git a/spec/controllers/get_related_resource_spec.rb b/spec/controllers/get_related_resource_spec.rb new file mode 100644 index 0000000..82ca245 --- /dev/null +++ b/spec/controllers/get_related_resource_spec.rb @@ -0,0 +1,79 @@ +require "rails_helper" + +RSpec.describe UsersController, type: :controller do + describe "#get_related_resource" do + let(:params) {{ + relationship: "user", + source: "posts", + post_id: post_id, + }} + + def do_request + get :get_related_resource, params_hash(params) + end + + + context "when the post does not exist" do + let(:post_id) { next_id Post } + before { do_request } + + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + end + + context "when the post exists" do + let!(:post) { Post.create! } + let(:post_id) { post.id } + + context "but the post has no user" do + before { do_request } + + it "responds with 200 OK" do + expect(response).to have_http_status 200 + end + + it "does not have user information" do + expect(body).to eq(data: nil) + end + end + + context "and the post has a user" do + let!(:user) { post.create_user! } + before { post.save } + + context "and Pundit allows the user to be viewed" do + before do + expect_any_instance_of(UserPolicy::Scope). + to receive(:resolve).and_return(User.all) + do_request + end + + it "responds with 200 OK" do + expect(response).to have_http_status 200 + end + + it "responds with the correct user" do + expect(body[:data][:id]).to eq user.id.to_s + end + end + + context "but Pundit does not allow the user to be viewed" do + before do + expect_any_instance_of(UserPolicy::Scope). + to receive(:resolve).and_return(User.none) + do_request + end + + it "responds with 200 OK" do + expect(response).to have_http_status 200 + end + + it "does not have user information" do + expect(body).to eq(data: nil) + end + end + end + end + end +end diff --git a/spec/controllers/get_related_resources_spec.rb b/spec/controllers/get_related_resources_spec.rb new file mode 100644 index 0000000..653bc9a --- /dev/null +++ b/spec/controllers/get_related_resources_spec.rb @@ -0,0 +1,54 @@ +require "rails_helper" + +RSpec.describe PostsController, type: :controller do + describe "#get_related_resources" do + let(:params) {{ + source: "users", + relationship: "posts", + user_id: user_id, + }} + + def do_request + get :get_related_resources, params_hash(params) + end + + describe "when the user does not exist" do + let(:user_id) { next_id User } + + before { do_request } + + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + end + + describe "when the user exists" do + let(:user) { User.create! } + let(:user_id) { user.id } + let(:posts) { 4.times.map { Post.create! } } + + before do + posts.first(2).map { |p| p.update!(user: user) } + + expect_any_instance_of(UserPolicy::Scope). + to receive(:resolve).and_return(User.all) + + # Make the scope return one post that belongs to the user + # and one that does not, so it can be tested that only the ones that + # belong to the user are eventually returned. + expect_any_instance_of(PostPolicy::Scope).to receive(:resolve). + and_return(Post.where(id: posts.values_at(1, 3).map(&:id))) + + do_request + end + + it "responds with 200 OK" do + expect(response).to have_http_status 200 + end + + it "uses the pundit scope and returns only those belonging to the user" do + expect(body[:data].map { |l| l[:id] }).to eq [posts[1].id.to_s] + end + end + end +end diff --git a/spec/controllers/index_spec.rb b/spec/controllers/index_spec.rb new file mode 100644 index 0000000..ba6a27a --- /dev/null +++ b/spec/controllers/index_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe UsersController, type: :controller do + describe "#index" do + # Make sure there are multiple users in the database, + # and select one at random that will feature in the random scope + # returned by the policy. + let!(:user) { 3.times.map { User.create! }.sample } + + before do + # Stub policy to return a random scope that could only result from here + expect_any_instance_of(UserPolicy::Scope). + to receive(:resolve).and_return(User.where(id: user.id)) + + get :index + end + + it "uses the Pundit scope" do + unless response.status == 200 + body[:errors].each do |error| + puts error[:meta][:exception] + puts error[:meta][:backtrace] + end + fail "Expected 200 OK but was #{response.status}" + end + + expect(body[:data].count).to eq 1 + expect(body.dig(:data, 0, :id)).to eq user.id.to_s + end + end +end diff --git a/spec/controllers/show_relationship_spec.rb b/spec/controllers/show_relationship_spec.rb new file mode 100644 index 0000000..75d8cbc --- /dev/null +++ b/spec/controllers/show_relationship_spec.rb @@ -0,0 +1,53 @@ +require "rails_helper" + +RSpec.describe UsersController, type: :controller do + describe "#show_relationship" do + let(:params) {{ + relationship: "posts", + user_id: user_id, + }} + + def do_request + get :show_relationship, params_hash(params) + end + + describe "when the user does not exist" do + let(:user_id) { next_id User } + + before { do_request } + + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + end + + describe "when the user exists" do + let(:user) { User.create! } + let(:user_id) { user.id } + let(:posts) { 4.times.map { Post.create! } } + + before do + posts.first(2).map { |p| p.update!(user: user) } + + expect_any_instance_of(UserPolicy::Scope). + to receive(:resolve).and_return(User.all) + + # Make the scope return one post that belongs to the user + # and one that does not, so it can be tested that only the ones that + # belong to the user are eventually returned. + expect_any_instance_of(PostPolicy::Scope).to receive(:resolve). + and_return(Post.where(id: posts.values_at(1, 3).map(&:id))) + + do_request + end + + it "responds with 200 OK" do + expect(response).to have_http_status 200 + end + + it "uses the pundit scope and returns only those belonging to the user" do + expect(body[:data].map { |l| l[:id] }).to eq [posts[1].id.to_s] + end + end + end +end diff --git a/spec/controllers/show_spec.rb b/spec/controllers/show_spec.rb new file mode 100644 index 0000000..37830c3 --- /dev/null +++ b/spec/controllers/show_spec.rb @@ -0,0 +1,63 @@ +require "rails_helper" + +RSpec.describe UsersController, type: :controller do + describe "#show" do + def do_request + get :show, params_hash(id: id) + end + + context "when the user does not exist" do + let(:id) { next_id User } + + before { do_request } + + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + end + + context "when the user exists" do + let!(:user) { User.create! } + let(:id) { user.id } + + it "uses the scope instead of calling #show?" do + expect_any_instance_of(UserPolicy::Scope).to receive(:resolve) + do_request + end + + context "but Pundit says no" do + before do + allow_any_instance_of(UserPolicy::Scope). + to receive(:resolve).and_return(User.none) + do_request + end + + # Even though the resource exists, it is still correct to respond with + # 404 Not Found because the client shouldn't be able to determine + # whether a given resource exists. + # + # From RFC 2616: + # + # > If the server does not wish to make this information available to + # > the client, the status code 404 (Not Found) can be used instead. + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + expect(body[:errors].count).to eq 1 + end + end + + context "and Pundit says yes" do + before do + allow_any_instance_of(UserPolicy::Scope). + to receive(:resolve).and_return(User.all) + do_request + end + + it "responds with 200 OK" do + expect(response).to have_http_status 200 + expect(body[:data][:id]).to eq user.id.to_s + end + end + end + end +end diff --git a/spec/controllers/update_relationship_spec.rb b/spec/controllers/update_relationship_spec.rb new file mode 100644 index 0000000..f228be6 --- /dev/null +++ b/spec/controllers/update_relationship_spec.rb @@ -0,0 +1,64 @@ +require "rails_helper" + +RSpec.describe PostsController, type: :controller do + describe "#update_relationship" do + let(:params) {{ + relationship: "user", + post_id: post_id, + data: { + type: "users", + id: user_id.to_s, + }, + }} + + def do_request + patch :update_relationship, params_hash(params) + end + + context "when the post does not exist" do + let(:user_id) { User.create!.id } + let(:post_id) { next_id Post } + before { do_request } + + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + end + + context "when the post exists" do + let!(:post) { Post.create! } + let(:post_id) { post.id } + + let!(:user) { User.create! } + let(:user_id) { user.id } + + context "but Pundit does not allow updating the post" do + before do + allow_any_instance_of(PostPolicy). + to receive(:update?).and_return(false) + do_request + end + + it "responds with 404 Forbidden" do + expect(response).to have_http_status 403 + end + end + + context "and Pundit allows updating the post" do + before do + allow_any_instance_of(PostPolicy). + to receive(:update?).and_return(true) + do_request + end + + it "responds with 204 No Content" do + expect(response).to have_http_status 204 + end + + it "updates post" do + expect(post.reload.user).to eq user + end + end + end + end +end diff --git a/spec/controllers/update_spec.rb b/spec/controllers/update_spec.rb new file mode 100644 index 0000000..82a8879 --- /dev/null +++ b/spec/controllers/update_spec.rb @@ -0,0 +1,96 @@ +require "rails_helper" + +RSpec.describe UsersController, type: :controller do + describe "#update" do + def do_request + data = { id: id, type: "users", attributes: { "created-at": Time.now } } + patch :update, params_hash(id: id, data: data) + end + + context "when the user does not exist" do + let(:id) { next_id User } + + before { do_request } + + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + end + + context "when the user exists" do + let!(:user) { User.create! } + let(:id) { user.id } + + it "uses the scope instead of calling #update?" do + expect_any_instance_of(UserPolicy::Scope).to receive(:resolve) + do_request + end + + # This should return 404 when the user can't see the resource, + # but 403 when it can. + context "but Pundit says no" do + before do + allow_any_instance_of(UserPolicy). + to receive(:update?).and_return(false) + end + + context "when the user is not included in the scope" do + before do + allow_any_instance_of(UserPolicy::Scope). + to receive(:resolve).and_return(User.none) + do_request + end + + # Even though the resource exists, it is still correct to respond with + # 404 Not Found because the client shouldn't be able to determine + # whether a given resource exists. + # + # From RFC 2616: + # + # > If the server does not wish to make this information available to + # > the client, the status code 404 (Not Found) can be used instead. + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + + it "contains an error in the JSON response" do + expect(body[:errors].count).to eq 1 + end + end + + context "when the user is included in the scope" do + before do + allow_any_instance_of(UserPolicy::Scope). + to receive(:resolve).and_return(User.all) + do_request + end + + it "responds with 403 Forbidden" do + expect(response).to have_http_status 403 + end + + it "contains an error in the JSON response" do + expect(body[:errors].count).to eq 1 + expect(body.dig(:errors, 0, :title)).to eq "Update Forbidden" + expect(body.dig(:errors, 0, :detail)).to eq <<-DESC.strip + You don't have permission to update this user. + DESC + end + end + end + + context "and Pundit says yes" do + before do + allow_any_instance_of(UserPolicy). + to receive(:update?).and_return(true) + do_request + end + + it "responds with 200 OK" do + expect(response).to have_http_status 200 + expect(body[:data][:id]).to eq user.id.to_s + end + end + end + end +end diff --git a/spec/controllers/users/create_relationship_spec.rb b/spec/controllers/users/create_relationship_spec.rb new file mode 100644 index 0000000..a48d083 --- /dev/null +++ b/spec/controllers/users/create_relationship_spec.rb @@ -0,0 +1,82 @@ +require "rails_helper" + +RSpec.describe UsersController, type: :controller do + describe "#create_relationship" do + let(:params) {{ + user_id: user_id, + relationship: "posts", + data: [{ type: "posts", id: post_id }], + }} + + let(:user) { User.create! } + let(:_post) { Post.create! } + + let(:user_id) { user.id } + let(:post_id) { _post.id } + + def do_request + post :create_relationship, params_hash(params) + end + + context "when the user does not exist" do + let(:user_id) { next_id User } + + before { do_request } + + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + end + + context "when the post does not exist" do + let(:post_id) { next_id Post } + + before { do_request } + + it "responds with 404 Not Found" do + expect(response).to have_http_status 404 + end + end + + context "when the user exists" do + context "but Pundit says no" do + before do + Post.destroy_all + expect_any_instance_of(PostPolicy). + to receive(:update?).and_return(false) + end + + it "does not create a post" do + expect { do_request }.not_to change { user.posts.count } + end + + it "responds with 403 Forbidden" do + do_request + expect(response).to have_http_status 403 + expect(body.dig(:errors, 0, :title)).to eq <<-TITLE.strip + Create Relationship Forbidden + TITLE + expect(body.dig(:errors, 0, :detail)).to eq <<-DESC.strip + You don't have permission to create relationship this post. + DESC + end + end + + context "and Pundit says yes" do + before do + expect_any_instance_of(PostPolicy). + to receive(:update?).and_return(true) + end + + it "responds with 204 No Content" do + do_request + expect(response).to have_http_status 204 + end + + it "creates a post" do + expect { do_request }.to change { user.posts.count }.by 1 + end + end + end + end +end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb deleted file mode 100644 index 259dd60..0000000 --- a/spec/controllers/users_controller_spec.rb +++ /dev/null @@ -1,330 +0,0 @@ -require "rails_helper" - -# This spec tests that a correctly configured controller and resource pair -# will automatically use Pundit policies. -RSpec.describe UsersController, type: :controller do - render_views - - it { is_expected.to be_a JSONAPI::ResourceController } - it { is_expected.to be_a Pundit::ResourceController } - - let(:body) { JSON.parse(response.body, symbolize_names: true) } - - describe "#index" do - # Make sure there are multiple users in the database, - # and select one at random that will feature in the random scope - # returned by the policy. - let!(:user) { 3.times.map { User.create! }.sample } - - before do - # Stub policy to return a random scope that could only result from here - expect_any_instance_of(UserPolicy::Scope). - to receive(:resolve).and_return(User.where(id: user.id)) - - get :index - end - - it "uses the Pundit scope" do - unless response.status == 200 - body[:errors].each do |error| - puts error[:meta][:exception] - puts error[:meta][:backtrace] - end - fail "Expected 200 OK but was #{response.status}" - end - - expect(body[:data].count).to eq 1 - expect(body.dig(:data, 0, :id)).to eq user.id.to_s - end - end - - describe "#create" do - def do_request - post :create, params_hash(data: { type: :users }) - end - - before do - request.headers["Content-Type"] = "application/vnd.api+json" - end - - context "but Pundit says no" do - before do - expect_any_instance_of(UserPolicy). - to receive(:create?).and_return(false) - end - - it "does not create a user" do - expect { do_request }.not_to change { User.count } - end - - it "responds with 403 Forbidden" do - do_request - expect(response).to have_http_status 403 - expect(body.dig(:errors, 0, :title)).to eq "Create Forbidden" - expect(body.dig(:errors, 0, :detail)).to eq <<-DESC.strip - You don't have permission to create this user. - DESC - end - end - - context "and Pundit says yes" do - before do - allow_any_instance_of(UserPolicy).to receive(:create?).and_return(true) - end - - it "creates a user" do - expect { do_request }.to change { User.count }.by 1 - end - - it "responds with 201 Created" do - do_request - expect(response).to have_http_status 201 - end - end - end - - describe "#show" do - def do_request - get :show, params_hash(id: id) - end - - context "when the user does not exist" do - let(:id) { User.order(:id).select(:id).first&.id.to_i + 1 } - - before { do_request } - - it "responds with 404 Not Found" do - expect(response).to have_http_status 404 - end - end - - context "when the user exists" do - let!(:user) { User.create! } - let(:id) { user.id } - - it "uses the scope instead of calling #show?" do - expect_any_instance_of(UserPolicy::Scope).to receive(:resolve) - do_request - end - - context "but Pundit says no" do - before do - allow_any_instance_of(UserPolicy::Scope). - to receive(:resolve).and_return(User.none) - do_request - end - - # Even though the resource exists, it is still correct to respond with - # 404 Not Found because the client shouldn't be able to determine - # whether a given resource exists. - # - # From RFC 2616: - # - # > If the server does not wish to make this information available to - # > the client, the status code 404 (Not Found) can be used instead. - it "responds with 404 Not Found" do - expect(response).to have_http_status 404 - expect(body[:errors].count).to eq 1 - end - end - - context "and Pundit says yes" do - before do - allow_any_instance_of(UserPolicy::Scope). - to receive(:resolve).and_return(User.all) - do_request - end - - it "responds with 200 OK" do - expect(response).to have_http_status 200 - expect(body[:data][:id]).to eq user.id.to_s - end - end - end - end - - describe "#update" do - def do_request - data = { id: id, type: "users", attributes: { "created-at": Time.now } } - patch :update, params_hash(id: id, data: data) - end - - before do - request.headers["Content-Type"] = "application/vnd.api+json" - end - - context "when the user does not exist" do - let(:id) { User.order(:id).select(:id).first&.id.to_i + 1 } - - before { do_request } - - it "responds with 404 Not Found" do - expect(response).to have_http_status 404 - end - end - - context "when the user exists" do - let!(:user) { User.create! } - let(:id) { user.id } - - it "uses the scope instead of calling #update?" do - expect_any_instance_of(UserPolicy::Scope).to receive(:resolve) - do_request - end - - # This should return 404 when the user can't see the resource, - # but 403 when it can. - context "but Pundit says no" do - before do - allow_any_instance_of(UserPolicy). - to receive(:update?).and_return(false) - end - - context "when the user is not included in the scope" do - before do - allow_any_instance_of(UserPolicy::Scope). - to receive(:resolve).and_return(User.none) - do_request - end - - # Even though the resource exists, it is still correct to respond with - # 404 Not Found because the client shouldn't be able to determine - # whether a given resource exists. - # - # From RFC 2616: - # - # > If the server does not wish to make this information available to - # > the client, the status code 404 (Not Found) can be used instead. - it "responds with 404 Not Found" do - expect(response).to have_http_status 404 - end - - it "contains an error in the JSON response" do - expect(body[:errors].count).to eq 1 - end - end - - context "when the user is included in the scope" do - before do - allow_any_instance_of(UserPolicy::Scope). - to receive(:resolve).and_return(User.all) - do_request - end - - it "responds with 403 Forbidden" do - expect(response).to have_http_status 403 - end - - it "contains an error in the JSON response" do - expect(body[:errors].count).to eq 1 - expect(body.dig(:errors, 0, :title)).to eq "Update Forbidden" - expect(body.dig(:errors, 0, :detail)).to eq <<-DESC.strip - You don't have permission to update this user. - DESC - end - end - end - - context "and Pundit says yes" do - before do - allow_any_instance_of(UserPolicy). - to receive(:update?).and_return(true) - do_request - end - - it "responds with 200 OK" do - expect(response).to have_http_status 200 - expect(body[:data][:id]).to eq user.id.to_s - end - end - end - end - - describe "#destroy" do - def do_request - delete :destroy, params_hash(id: id) - end - - context "when the user does not exist" do - let(:id) { User.order(:id).select(:id).first&.id.to_i + 1 } - - before { do_request } - - it "responds with 404 Not Found" do - expect(response).to have_http_status 404 - end - end - - context "when the user exists" do - let!(:user) { User.create! } - let(:id) { user.id } - - context "but Pundit says no" do - before do - allow_any_instance_of(UserPolicy). - to receive(:destroy?).and_return(false) - end - - context "when the user is not included in the scope" do - before do - allow_any_instance_of(UserPolicy::Scope). - to receive(:resolve).and_return(User.none) - do_request - end - - # Even though the resource exists, it is still correct to respond with - # 404 Not Found because the client shouldn't be able to determine - # whether a given resource exists. - # - # From RFC 2616: - # - # > If the server does not wish to make this information available to - # > the client, the status code 404 (Not Found) can be used instead. - it "responds with 404 Not Found" do - expect(response).to have_http_status 404 - end - - it "contains an error in the JSON response" do - expect(body[:errors].count).to eq 1 - end - end - - context "when the user is included in the scope" do - before do - allow_any_instance_of(UserPolicy::Scope). - to receive(:resolve).and_return(User.all) - do_request - end - - it "responds with 403 Forbidden" do - expect(response).to have_http_status 403 - end - - it "contains an error in the JSON response" do - expect(body[:errors].count).to eq 1 - expect(body.dig(:errors, 0, :title)).to eq "Destroy Forbidden" - expect(body.dig(:errors, 0, :detail)).to eq <<-DESC.strip - You don't have permission to destroy this user. - DESC - end - end - end - - context "and Pundit says yes" do - before do - allow_any_instance_of(UserPolicy). - to receive(:destroy?).and_return(true) - end - - it "destroys the user" do - expect { do_request }.to change { User.count }.by(-1) - end - - it "responds with 204 No Content" do - do_request - expect(response).to have_http_status 204 - end - end - end - end -end diff --git a/spec/dummy/app/controllers/application_controller.rb b/spec/dummy/app/controllers/application_controller.rb new file mode 100644 index 0000000..9a28bba --- /dev/null +++ b/spec/dummy/app/controllers/application_controller.rb @@ -0,0 +1,6 @@ +class ApplicationController < JSONAPI::ResourceController + include Pundit::ResourceController + + def current_user + end +end diff --git a/spec/dummy/app/controllers/posts_controller.rb b/spec/dummy/app/controllers/posts_controller.rb new file mode 100644 index 0000000..a66e6b8 --- /dev/null +++ b/spec/dummy/app/controllers/posts_controller.rb @@ -0,0 +1,2 @@ +class PostsController < ApplicationController +end diff --git a/spec/dummy/app/controllers/users_controller.rb b/spec/dummy/app/controllers/users_controller.rb index e605683..3e74dea 100644 --- a/spec/dummy/app/controllers/users_controller.rb +++ b/spec/dummy/app/controllers/users_controller.rb @@ -1,6 +1,2 @@ -class UsersController < JSONAPI::ResourceController - include Pundit::ResourceController - - def current_user - end +class UsersController < ApplicationController end diff --git a/spec/dummy/app/models/user.rb b/spec/dummy/app/models/user.rb index 8a0e2b9..3f88708 100644 --- a/spec/dummy/app/models/user.rb +++ b/spec/dummy/app/models/user.rb @@ -2,4 +2,6 @@ class User < ApplicationRecord def x_post Post.find_or_create_by!(title: "Hello") end + + has_many :posts end diff --git a/spec/dummy/app/policies/application_policy.rb b/spec/dummy/app/policies/application_policy.rb new file mode 100644 index 0000000..b1b0e93 --- /dev/null +++ b/spec/dummy/app/policies/application_policy.rb @@ -0,0 +1,25 @@ +class ApplicationPolicy + attr_reader :user, :record + + def initialize(user, record) + @user = user + @record = record + end + + def scope + Pundit.policy_scope!(user, record.class) + end + + class Scope + attr_reader :user, :scope + + def initialize(user, scope) + @user = user + @scope = scope + end + + def resolve + scope + end + end +end diff --git a/spec/dummy/app/policies/post_policy.rb b/spec/dummy/app/policies/post_policy.rb index 2c165a2..fb69956 100644 --- a/spec/dummy/app/policies/post_policy.rb +++ b/spec/dummy/app/policies/post_policy.rb @@ -1,25 +1,12 @@ -class PostPolicy - attr_reader :user, :record - - def initialize(user, record) - @user = user - @record = record +class PostPolicy < ApplicationPolicy + def update? + false end - def scope - Pundit.policy_scope!(user, record.class) + def destroy? + false end - class Scope - attr_reader :user, :scope - - def initialize(user, scope) - @user = user - @scope = scope - end - - def resolve - scope - end + class Scope < Scope end end diff --git a/spec/dummy/app/policies/user_policy.rb b/spec/dummy/app/policies/user_policy.rb index ce4cf3a..0f59b67 100644 --- a/spec/dummy/app/policies/user_policy.rb +++ b/spec/dummy/app/policies/user_policy.rb @@ -1,11 +1,4 @@ -class UserPolicy - attr_reader :user, :record - - def initialize(user, record) - @user = user - @record = record - end - +class UserPolicy < ApplicationPolicy def create? false end @@ -18,20 +11,6 @@ def destroy? false end - def scope - Pundit.policy_scope!(user, record.class) - end - - class Scope - attr_reader :user, :scope - - def initialize(user, scope) - @user = user - @scope = scope - end - - def resolve - scope - end + class Scope < Scope end end diff --git a/spec/dummy/app/resources/application_resource.rb b/spec/dummy/app/resources/application_resource.rb new file mode 100644 index 0000000..82bfec3 --- /dev/null +++ b/spec/dummy/app/resources/application_resource.rb @@ -0,0 +1,5 @@ +class ApplicationResource < JSONAPI::Resource + include Pundit::Resource + + abstract +end diff --git a/spec/dummy/app/resources/post_resource.rb b/spec/dummy/app/resources/post_resource.rb index 176f02b..3d0ed09 100644 --- a/spec/dummy/app/resources/post_resource.rb +++ b/spec/dummy/app/resources/post_resource.rb @@ -1,3 +1,3 @@ -class PostResource < JSONAPI::Resource - include Pundit::Resource +class PostResource < ApplicationResource + has_one :user end diff --git a/spec/dummy/app/resources/user_resource.rb b/spec/dummy/app/resources/user_resource.rb index 48a62c3..c09e41c 100644 --- a/spec/dummy/app/resources/user_resource.rb +++ b/spec/dummy/app/resources/user_resource.rb @@ -1,10 +1,10 @@ -class UserResource < JSONAPI::Resource - include Pundit::Resource - +class UserResource < ApplicationResource attribute :created_at # include relationship with and without relation_name: # to check both cases are handled has_one :x_post, class_name: "Post" has_one :post, relation_name: :x_post, class_name: "Post" + + has_many :posts end diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index c4d50db..0203801 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -10,6 +10,9 @@ class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. + if ActiveRecord::Base.respond_to?(:belongs_to_required_by_default=) + config.active_record.belongs_to_required_by_default = false + end end end diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 4cc3427..4b66c04 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html jsonapi_resources :users + jsonapi_resources :posts end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index ac6171d..d5f3a93 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -8,20 +8,7 @@ # Add additional requires below this line. Rails is not loaded until this point! Dir["#{__dir__}/support/types/**/*.rb"].each { |f| require f } -# Requires supporting ruby files with custom matchers and macros, etc, in -# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are -# run as spec files by default. This means that files in spec/support that end -# in _spec.rb will both be required and run as specs, causing the specs to be -# run twice. It is recommended that you do not name files matching this glob to -# end with _spec.rb. You can configure this pattern with the --pattern -# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. -# -# The following line is provided for convenience purposes. It has the downside -# of increasing the boot-up time by auto-requiring all files in the support -# directory. Alternatively, in the individual `*_spec.rb` files, manually -# require only the support files necessary. -# -# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } +Dir["#{__dir__}/support/types/**/*.rb"].each { |f| require f } # Checks for pending migration and applies them before tests are run. # If you are not using ActiveRecord, you can remove this line. diff --git a/spec/support/types/controller.rb b/spec/support/types/controller.rb index 3bc6d7c..9a131c5 100644 --- a/spec/support/types/controller.rb +++ b/spec/support/types/controller.rb @@ -1,4 +1,8 @@ RSpec.shared_context "controller specs", type: :controller do + render_views + + let(:body) { JSON.parse(response.body, symbolize_names: true) } + def params_hash(inner_hash) if Rails.version.split(?.).first.to_i < 5 inner_hash @@ -6,4 +10,12 @@ def params_hash(inner_hash) { params: inner_hash } end end + + before do + request.headers["Content-Type"] = "application/vnd.api+json" + end + + def next_id(model_class) + model_class.order(:id).select(:id).first&.id.to_i + 1 + end end