From 85a1eacbde06e80f08644b692c78071910b87078 Mon Sep 17 00:00:00 2001 From: James Mead Date: Sun, 25 May 2025 16:17:25 +0100 Subject: [PATCH 1/3] Move before below lets in updating project feature spec This is more idiomatic and thus easier to read. --- spec/features/project/updating_a_project_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/features/project/updating_a_project_spec.rb b/spec/features/project/updating_a_project_spec.rb index 41d325ce5..e46efabe6 100644 --- a/spec/features/project/updating_a_project_spec.rb +++ b/spec/features/project/updating_a_project_spec.rb @@ -3,12 +3,6 @@ require 'rails_helper' RSpec.describe 'Updating a project', type: :request do - before do - authenticated_in_hydra_as(owner) - - create(:component, project:, name: 'main', extension: 'py', content: 'print("hi")') - end - let(:headers) { { Authorization: UserProfileMock::TOKEN } } let!(:project) { create(:project, name: 'Test Project', user_id: owner.id) } let(:owner) { create(:owner, school:) } @@ -25,6 +19,12 @@ } end + before do + authenticated_in_hydra_as(owner) + + create(:component, project:, name: 'main', extension: 'py', content: 'print("hi")') + end + it 'responds 200 OK' do put("/api/projects/#{project.id}", headers:, params:) expect(response).to have_http_status(:ok) From 27bd1bdef19263e34d9140a05e825a18ec4ab149 Mon Sep 17 00:00:00 2001 From: James Mead Date: Sun, 25 May 2025 16:54:35 +0100 Subject: [PATCH 2/3] Fix updating project feature spec Previously this spec was not representative of how the API was being used in practice and it was only really working by accident. The PUT requests in the examples were using the `Project#id` rather than the `Project#identifier` as they should have been. The spec was working even though the `Api::ProjectsController#load_project` before action was *not* finding the project, because the `load_and_authorize_resource` before action was then finding the project using `Project.find(params[:id])` and setting the `@project` instance variable used by the rest of the logic in the controller. However, clients of editor-api like editor-standalone use the project identifier as the resource ID in the URL path [1], so this spec was completely unrepresentative of how the endpoint was really being used. In this commit I've changed the examples to use the `Project#identifier` as the resource ID in the PUT requests. This means that the `Api::ProjectsController#load_project` before action now finds the project by identifier and since it sets the `@project` instance variable the `load_and_authorize_resource` before action no longer attempts to load the resource, it just does the authorize step using the already loaded project. In order to make this work reliably, I had to explicitly set the `Project#locale` to one of the fallback locales in `ProjectLoader` [2], i.e. 'en' or `nil`. Otherwise, the random locale selected in the project factory [3] meant that sometimes the `load_project` before action (which uses the `ProjectLoader` did not find the project and the `load_and_authorize_resource` before action raised an `ActiveRecord::RecordNotFound` resulting in a 404 Not Found. Now the spec is no longer making use of the randome locale from the factory, I've taken the opportunity to add some new examples demonstrating the behaviour when the project has different locales. This effectively demonstrates that `load_project` is wired up to `ProjectLoader` correctly. As an aside, I'm not convinced that having the factory select a locale at random is a good idea. I found it very confusing when it led to undeterministic specs. However, I'll leave that for another time. [1]: https://github.com/RaspberryPiFoundation/editor-standalone/blob/1d4375635cb6890794732072d608dbd4b05b3bb0/src/utils/apiCallHandler/projects.js#L16 [2]: https://github.com/RaspberryPiFoundation/editor-api/blob/b4bd337d09a88b1f41ecdc13136f7d11da0dcf89/lib/project_loader.rb#L8 [3]: https://github.com/RaspberryPiFoundation/editor-api/blob/b4bd337d09a88b1f41ecdc13136f7d11da0dcf89/spec/factories/project.rb#L9 --- .../project/updating_a_project_spec.rb | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/spec/features/project/updating_a_project_spec.rb b/spec/features/project/updating_a_project_spec.rb index e46efabe6..f1717f04c 100644 --- a/spec/features/project/updating_a_project_spec.rb +++ b/spec/features/project/updating_a_project_spec.rb @@ -4,7 +4,8 @@ RSpec.describe 'Updating a project', type: :request do let(:headers) { { Authorization: UserProfileMock::TOKEN } } - let!(:project) { create(:project, name: 'Test Project', user_id: owner.id) } + let(:locale) { 'en' } + let!(:project) { create(:project, name: 'Test Project', user_id: owner.id, locale:) } let(:owner) { create(:owner, school:) } let(:school) { create(:school) } @@ -26,31 +27,54 @@ end it 'responds 200 OK' do - put("/api/projects/#{project.id}", headers:, params:) + put("/api/projects/#{project.identifier}", headers:, params:) expect(response).to have_http_status(:ok) end it 'responds with the project JSON' do - put("/api/projects/#{project.id}", headers:, params:) + put("/api/projects/#{project.identifier}", headers:, params:) data = JSON.parse(response.body, symbolize_names: true) expect(data[:name]).to eq('New Name') end it 'responds with the components JSON' do - put("/api/projects/#{project.id}", headers:, params:) + put("/api/projects/#{project.identifier}", headers:, params:) data = JSON.parse(response.body, symbolize_names: true) expect(data[:components].first[:content]).to eq('print("hello")') end it 'responds 422 Unprocessable Entity when params are invalid' do - put("/api/projects/#{project.id}", headers:, params: { project: { components: [{ name: ' ' }] } }) + put("/api/projects/#{project.identifier}", headers:, params: { project: { components: [{ name: ' ' }] } }) expect(response).to have_http_status(:unprocessable_entity) end it 'responds 401 Unauthorized when no token is given' do - put("/api/projects/#{project.id}", params:) + put("/api/projects/#{project.identifier}", params:) expect(response).to have_http_status(:unauthorized) end + + context 'when locale is nil, i.e. the other fallback locale in ProjectLoader' do + let(:locale) { nil } + + it 'responds 200 OK even though no locale is specified in query string' do + put("/api/projects/#{project.identifier}", headers:, params:) + expect(response).to have_http_status(:ok) + end + end + + context "when locale is 'fr', i.e. not a fallback locale in ProjectLoader" do + let(:locale) { 'fr' } + + it 'responds 200 OK if locale is specified in query string' do + put("/api/projects/#{project.identifier}?locale=fr", headers:, params:) + expect(response).to have_http_status(:ok) + end + + it 'responds 404 Not Found if locale is not specified in query string' do + put("/api/projects/#{project.identifier}", headers:, params:) + expect(response).to have_http_status(:not_found) + end + end end From ed1be1bef6b50cbec479c7dcb9e6735f9e6275b8 Mon Sep 17 00:00:00 2001 From: James Mead Date: Sun, 25 May 2025 17:22:49 +0100 Subject: [PATCH 3/3] Don't load resource via CanCanCan in Api::ProjectsController As I explained in the previous commit, the `load_project` before action is where we want the project to be loaded, i.e. via `ProjectLoader` so that it's found by a combination of `Project#identifier` and `Project#locale`. To make this clearer, I've changed the `load_and_authorize_resource` before action to `authorize_resource` [1], so the CanCanCan authorization uses the project found by the `load_project` before action. However, this meant that if the project was *not* found by the `load_project` before action an exception was raised in `Project::Update.call` resulting in a 422 Unprocessable Entity response with the following error message: Error persisting changes: undefined method `components' for nil:NilClass To fix this I'm now raising an `ActiveRecord::RecordNotFound` exception in the `load_project` before action if no project is found. This results in the expected 404 Not Found response. I think there's a strong case to be made the this exception raising behaviour should be added to `ProjectLoader#load`. However, that's a bigger change with a lot more risk, so I'm going to leave that for now. Note that I've retained the load resource functionality for the `create` action, because the `load_project` before action isn't triggered for `create` and the authorize resource functionality seems to rely on the project built by the load resource step and I want to keep changes to a minimum. [1]: https://github.com/CanCanCommunity/cancancan/blob/3.4.0/docs/controller_helpers.md#authorize_resource-load_resource-load_and_authorize_resource --- app/controllers/api/projects_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index f44710faf..b1b5fbffc 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -7,7 +7,8 @@ class ProjectsController < ApiController before_action :authorize_user, only: %i[create update index destroy] before_action :load_project, only: %i[show update destroy show_context] before_action :load_projects, only: %i[index] - load_and_authorize_resource + load_resource only: :create + authorize_resource before_action :verify_lesson_belongs_to_school, only: :create after_action :pagination_link_header, only: %i[index] @@ -73,6 +74,7 @@ def load_project else project_loader.load end + raise ActiveRecord::RecordNotFound if @project.blank? end def load_projects