From dac534557d8da05d52ffed3cb33a6ac7b766e217 Mon Sep 17 00:00:00 2001 From: Fraser Speirs Date: Thu, 26 Feb 2026 10:57:45 +0000 Subject: [PATCH] Make load_and_authorize_remix deterministic. This commit changes RemixesController#load_and_authorize_remix to deterministically always return the most recent Project record based on created_at and, secondarily, updated_at dates. While, in principle, there should not be multiple records where (remixed_from_id:, user_id:) are the same, in practice there are and there is no database constraint to prevent there being. The previous code would nondeterministically return one of possibly many records, causing downstream errors in Experience CS since each would have a different identifier, not matching the one stored in Experience CS. Overarching epic: https://github.com/RaspberryPiFoundation/experience-cs/issues/1826 --- .../api/projects/remixes_controller.rb | 4 ++- spec/requests/projects/remix_spec.rb | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/projects/remixes_controller.rb b/app/controllers/api/projects/remixes_controller.rb index 222279f7e..1df7a366a 100644 --- a/app/controllers/api/projects/remixes_controller.rb +++ b/app/controllers/api/projects/remixes_controller.rb @@ -44,7 +44,9 @@ def project end def load_and_authorize_remix - @project = Project.find_by!(remixed_from_id: project.id, user_id: current_user&.id) + @project = Project.where(remixed_from_id: project.id, user_id: current_user&.id) + .order(created_at: :desc, updated_at: :desc) + .first! authorize! :show, @project end diff --git a/spec/requests/projects/remix_spec.rb b/spec/requests/projects/remix_spec.rb index 1443deca5..0caefd1be 100644 --- a/spec/requests/projects/remix_spec.rb +++ b/spec/requests/projects/remix_spec.rb @@ -68,6 +68,24 @@ expect(response).to have_http_status(:not_found) end + + context 'when multiple remixes exist for the same user and project' do + before do + create(:project, remixed_from_id: original_project.id, user_id: authenticated_user.id, + created_at: 2.days.ago, updated_at: 2.days.ago) + end + + let!(:newer_remix) do + create(:project, remixed_from_id: original_project.id, user_id: authenticated_user.id, + created_at: 1.hour.from_now, updated_at: 1.hour.from_now) + end + + it 'returns the most recently created remix' do + get("/api/projects/#{original_project.identifier}/remix", headers:) + + expect(response.parsed_body['identifier']).to eq(newer_remix.identifier) + end + end end describe('#show_identifier') do @@ -97,6 +115,23 @@ get("/api/projects/#{original_project.identifier}/remix/identifier", headers:) expect(response).to have_http_status(:not_found) end + + context 'when multiple remixes exist for the same user and project' do + before do + create(:project, remixed_from_id: original_project.id, user_id: authenticated_user.id, + created_at: 2.days.ago, updated_at: 2.days.ago) + end + + let!(:newer_remix) do + create(:project, remixed_from_id: original_project.id, user_id: authenticated_user.id, + created_at: 1.hour.from_now, updated_at: 1.hour.from_now) + end + + it 'returns the identifier of the most recently created remix' do + get("/api/projects/#{original_project.identifier}/remix/identifier", headers:) + expect(response.parsed_body['identifier']).to eq(newer_remix.identifier) + end + end end describe '#create' do