From 4dfdd6ec5db601ac25552855e86c4bf76a68b50b Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Thu, 30 Apr 2026 09:01:11 +0300 Subject: [PATCH] Drop numeric pins on auxilary links --- .../work_packages/work_package_representer.rb | 4 ++-- .../context_menu_shared_examples.rb | 19 ++++++++------- .../work_package_representer_spec.rb | 23 +++++++++---------- .../work_package/semantic_identifier_spec.rb | 11 +++++---- .../v3/work_packages/show_resource_spec.rb | 7 ++++++ 5 files changed, 37 insertions(+), 27 deletions(-) diff --git a/lib/api/v3/work_packages/work_package_representer.rb b/lib/api/v3/work_packages/work_package_representer.rb index f2db7aab120..9f2411947dc 100644 --- a/lib/api/v3/work_packages/work_package_representer.rb +++ b/lib/api/v3/work_packages/work_package_representer.rb @@ -106,7 +106,7 @@ module API next if represented.new_record? { - href: new_work_package_move_path(work_package_id: represented.id), + href: new_work_package_move_path(represented), type: "text/html", title: "Move work package '#{represented.subject}'" } @@ -117,7 +117,7 @@ module API next if represented.new_record? { - href: copy_work_package_path(id: represented.id), + href: work_package_path(represented, "copy"), type: "text/html", title: "Copy work package '#{represented.subject}'" } diff --git a/spec/features/work_packages/table/context_menu/context_menu_shared_examples.rb b/spec/features/work_packages/table/context_menu/context_menu_shared_examples.rb index 9573079ff9b..cf101beb90e 100644 --- a/spec/features/work_packages/table/context_menu/context_menu_shared_examples.rb +++ b/spec/features/work_packages/table/context_menu/context_menu_shared_examples.rb @@ -100,18 +100,21 @@ RSpec.shared_examples_for "provides a single WP context menu" do context "with semantic identifiers enabled", with_flag: { semantic_work_package_ids: true }, with_settings: { work_packages_identifier: "semantic" } do + # The shared project is created classic-style (lowercase identifier), but + # semantic mode requires the uppercase format. Rewrite it before any + # example-scoped setup so work-package allocation and URL helpers both see + # a valid semantic prefix. Specs that create their own project per example + # should prefer the `:semantic` factory trait instead. + around do |example| + semantic_project_identifier = "PROJ#{project.id}".first(Projects::Identifier::SEMANTIC_IDENTIFIER_MAX_LENGTH) + project.update_columns(identifier: semantic_project_identifier) + example.run + end + it "uses numeric parent_id in the URL and sets the parent correctly" do expect(Setting::WorkPackageIdentifier.semantic_mode_active?) .to be(true), "expected semantic mode to be active via with_settings + with_flag metadata" - # Consumers of this shared example use shared_let(:project), which evaluates - # before per-example `with_settings:` activates semantic mode — so the project - # was created via the classic-mode factory path with a lowercase slug. Force - # an identifier that satisfies Projects::Identifier's semantic constraints - # (uppercase, [A-Z][A-Z0-9_]*, max 10 chars). For specs that create their own - # project per example, prefer the `:semantic` factory trait instead. - semantic_project_identifier = "PROJ#{work_package.project.id}".first(Projects::Identifier::SEMANTIC_IDENTIFIER_MAX_LENGTH) - work_package.project.update_columns(identifier: semantic_project_identifier) work_package.allocate_and_register_semantic_id if work_package.identifier.blank? open_context_menu.call diff --git a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb index def2bcae9cc..67f23757fd7 100644 --- a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb +++ b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb @@ -1204,7 +1204,7 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter do it_behaves_like "has a titled action link" do let(:link) { "move" } - let(:href) { "/work_packages/#{work_package.id}/move/new" } + let(:href) { "/work_packages/#{work_package.display_id}/move/new" } let(:permission) { :move_work_packages } let(:title) { "Move work package '#{work_package.subject}'" } end @@ -1214,7 +1214,7 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter do describe "copy" do it_behaves_like "has a titled action link" do let(:link) { "copy" } - let(:href) { "/work_packages/#{work_package.id}/copy" } + let(:href) { work_package_path(work_package, "copy") } let(:permission) { :add_work_packages } let(:title) { "Copy work package '#{work_package.subject}'" } end @@ -1226,7 +1226,7 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter do it_behaves_like "has a titled action link" do let(:link) { "copy" } - let(:href) { "/work_packages/#{work_package.id}/copy" } + let(:href) { "/work_packages/#{work_package.display_id}/copy" } let(:permission) { :add_work_packages } let(:title) { "Copy work package '#{work_package.subject}'" } end @@ -1261,33 +1261,32 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter do end end - # The HAL surface must produce numeric URLs regardless of identifier - # mode — external API consumers depend on stable, predictable IDs. - # These specs guard against a future change that drops the explicit - # `id:` kwargs (or model-positional pins) and silently lets URLs flip - # to semantic identifiers via WorkPackage#to_param. + # The HAL contract surface stays numeric in semantic mode so clients + # can correlate work packages by comparing href strings (one + # resource's `self` === another's `parent`, etc.). Auxiliary + # endpoints (`pdf`, `generate_pdf`, `atom`) follow the same convention. context "with semantic identifier mode active", with_flag: { semantic_work_package_ids: true }, with_settings: { work_packages_identifier: "semantic", feeds_enabled?: true } do let(:work_package) { build_stubbed(:work_package, identifier: "PROJ-7", project: workspace) } let(:permissions) { all_permissions + [:export_work_packages] } - it "self stays numeric" do + it "self href stays numeric" do expect(subject).to be_json_eql("/api/v3/work_packages/#{work_package.id}".to_json) .at_path("_links/self/href") end - it "pdf stays numeric" do + it "pdf href stays numeric" do expect(subject).to be_json_eql("/work_packages/#{work_package.id}.pdf".to_json) .at_path("_links/pdf/href") end - it "generate_pdf stays numeric" do + it "generate_pdf href stays numeric" do expect(subject).to be_json_eql("/work_packages/#{work_package.id}/generate_pdf_dialog".to_json) .at_path("_links/generate_pdf/href") end - it "atom stays numeric" do + it "atom href stays numeric" do expect(subject).to be_json_eql("/work_packages/#{work_package.id}.atom".to_json) .at_path("_links/atom/href") end diff --git a/spec/models/work_package/semantic_identifier_spec.rb b/spec/models/work_package/semantic_identifier_spec.rb index fc4d7624471..6620e2f04aa 100644 --- a/spec/models/work_package/semantic_identifier_spec.rb +++ b/spec/models/work_package/semantic_identifier_spec.rb @@ -466,8 +466,9 @@ RSpec.describe WorkPackage::SemanticIdentifier do end end - context "when semantic mode is not active", - with_flag: { semantic_work_package_ids: false } do + context "when classic mode is active", + with_flag: { semantic_work_package_ids: false }, + with_settings: { work_packages_identifier: "classic" } do it "returns the numeric id as a string" do expect(work_package.to_param).to eq(work_package.id.to_s) end @@ -475,10 +476,10 @@ RSpec.describe WorkPackage::SemanticIdentifier do it "makes work_package_path produce a numeric URL" do expect(work_package_path(work_package)).to end_with("/work_packages/#{work_package.id}") end - end - it "returns nil for new (unsaved) records" do - expect(WorkPackage.new.to_param).to be_nil + it "returns nil for new (unsaved) records" do + expect(WorkPackage.new.to_param).to be_nil + end end end diff --git a/spec/requests/api/v3/work_packages/show_resource_spec.rb b/spec/requests/api/v3/work_packages/show_resource_spec.rb index 63105f96f7b..5313a6207b3 100644 --- a/spec/requests/api/v3/work_packages/show_resource_spec.rb +++ b/spec/requests/api/v3/work_packages/show_resource_spec.rb @@ -221,6 +221,13 @@ RSpec.describe "API v3 Work package resource", context "with a semantic identifier", with_flag: { semantic_work_package_ids: true }, with_settings: { work_packages_identifier: "semantic" } do + let(:project) { create(:project, :semantic) } + let(:user) do + create(:user, member_with_permissions: { project => %i[view_work_packages] }) + end + let(:work_package) do + create(:work_package, project:, description: "lorem ipsum") + end let(:get_path) { api_v3_paths.work_package work_package.display_id } before do