From 8070c0894aadb3df20ca6ded0e02ced3f1101bdf Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Fri, 30 Oct 2026 16:36:45 +0000 Subject: [PATCH 1/6] install imagemagick in CI so rmagick builds Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2c16e78..63f83b20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,11 @@ jobs: steps: - uses: actions/checkout@v4 + - run: | + sudo apt-get update -qq + sudo apt-get install --no-install-recommends -y imagemagick libmagickwand-dev ghostscript + sudo sed -i '/disable ghostscript format types/,+6d' /etc/ImageMagick-6/policy.xml + - uses: ruby/setup-ruby@v1 with: ruby-version: "3.4.4" From 5cc4f3ab7d700acac62fca4d51aa0f79a4dc3fe6 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Fri, 30 Oct 2026 16:41:48 +0000 Subject: [PATCH 2/6] run specs without credentials: dummy lockbox keys in CI, stub USPS pricing Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 3 +++ spec/support/fake_usps_pricing.rb | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 spec/support/fake_usps_pricing.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63f83b20..e593d6ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,9 @@ jobs: env: RAILS_ENV: test DATABASE_URL: postgres://postgres:postgres@localhost:5432/theseus_test + # no master key in CI, so lockbox/blind_index need throwaway keys + LOCKBOX_MASTER_KEY: "0000000000000000000000000000000000000000000000000000000000000000" + BLIND_INDEX_MASTER_KEY: "0000000000000000000000000000000000000000000000000000000000000000" steps: - uses: actions/checkout@v4 diff --git a/spec/support/fake_usps_pricing.rb b/spec/support/fake_usps_pricing.rb new file mode 100644 index 00000000..407e5467 --- /dev/null +++ b/spec/support/fake_usps_pricing.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Letters price themselves on save through USPS::PricingEngine, which calls the +# live USPS API. Give every spec flat rates instead so the suite never touches +# the network. Specs that care about a rate can still stub over these. +RSpec.configure do |config| + config.before do + allow(USPS::PricingEngine).to receive(:metered_price).and_return(0.73) + allow(USPS::PricingEngine).to receive(:domestic_stamp_price).and_return(0.78) + allow(USPS::PricingEngine).to receive(:fcmi_price).and_return(1.65) + end +end From b8896eab453ddd04cd8372498c35ebf2cb211ad4 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Fri, 30 Oct 2026 16:44:41 +0000 Subject: [PATCH 3/6] set dummy HCB env vars in CI Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e593d6ce..8d56a6af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,10 @@ jobs: # no master key in CI, so lockbox/blind_index need throwaway keys LOCKBOX_MASTER_KEY: "0000000000000000000000000000000000000000000000000000000000000000" BLIND_INDEX_MASTER_KEY: "0000000000000000000000000000000000000000000000000000000000000000" + HCB_USPS_ORG_ID: org_ci_usps + HCB_WAREHOUSE_ORG_ID: org_ci_warehouse + HCB_CLIENT_ID: ci_client_id + HCB_CLIENT_SECRET: ci_client_secret steps: - uses: actions/checkout@v4 From 87617f3d1e47ddf4cc523ef503cb3cd8e1b61c02 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Fri, 30 Oct 2026 16:45:07 +0000 Subject: [PATCH 4/6] fix admin user picker subquery on warehouse batches index the base relation selects batches.* plus orders_count, so select(:user_id) appended a third column set to the IN subquery and postgres rejected it Co-Authored-By: Claude Opus 5.5 --- app/controllers/warehouse/batches_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/warehouse/batches_controller.rb b/app/controllers/warehouse/batches_controller.rb index d7b133bc..4e484f69 100644 --- a/app/controllers/warehouse/batches_controller.rb +++ b/app/controllers/warehouse/batches_controller.rb @@ -13,7 +13,7 @@ def index batches = all_batches batches = batches.where(user_id: params[:user_id]) if params[:user_id].present? && current_user&.is_admin? batches = batches.search(params[:search]) if params[:search].present? - users = current_user&.is_admin? ? User.where(id: all_batches.reorder(nil).select(:user_id).distinct).order(:email) : [] + users = current_user&.is_admin? ? User.where(id: all_batches.reorder(nil).unscope(:select).select(:user_id).distinct).order(:email) : [] render Views::Warehouse::Batches::Index.new( batches: batches, search: params[:search], From 330ae22581e4e1de13b947c81e0a2f2596d5279a Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Fri, 30 Oct 2026 16:48:18 +0000 Subject: [PATCH 5/6] fix the last CI failures: optional billing profile on MCP order update, credential-free specs the MCP warehouse_orders_update tool resolved billing_profile_id even when it wasn't passed, so every update without it failed Co-Authored-By: Claude Opus 5.5 --- app/toolboxes/warehouse/orders_toolbox.rb | 3 +-- spec/jobs/batch_process_job_spec.rb | 2 +- spec/requests/iv_mtr_webhook_spec.rb | 8 ++++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/toolboxes/warehouse/orders_toolbox.rb b/app/toolboxes/warehouse/orders_toolbox.rb index 53d7af9f..8193906b 100644 --- a/app/toolboxes/warehouse/orders_toolbox.rb +++ b/app/toolboxes/warehouse/orders_toolbox.rb @@ -95,9 +95,8 @@ def create_from_template param :address, :object, "Shipping address fields to update", optional: true end def update - billing_profile = resolve_billing_profile(params[:billing_profile_id]) update_attrs = order_params - update_attrs[:billing_profile] = billing_profile if params.key?(:billing_profile_id) + update_attrs[:billing_profile] = resolve_billing_profile(params[:billing_profile_id]) if params.key?(:billing_profile_id) @order.update!(update_attrs) render :show diff --git a/spec/jobs/batch_process_job_spec.rb b/spec/jobs/batch_process_job_spec.rb index a098c434..bf815a6a 100644 --- a/spec/jobs/batch_process_job_spec.rb +++ b/spec/jobs/batch_process_job_spec.rb @@ -142,7 +142,7 @@ def perform_job sent = hcb_disbursements.sole expect(sent[:profile]).to eq(hcb_account) - expect(sent[:amount_cents]).to eq((letters.sum(&:postage) * 100).ceil) + expect(sent[:amount_cents]).to eq(letters.sum { |l| (l.postage_for(postage_type: "indicia").to_d * 100).ceil }) expect(sent[:name]).to start_with("Postage for #{batch.public_id}") entry = batch.ledger_entries.sole diff --git a/spec/requests/iv_mtr_webhook_spec.rb b/spec/requests/iv_mtr_webhook_spec.rb index e928796f..15712828 100644 --- a/spec/requests/iv_mtr_webhook_spec.rb +++ b/spec/requests/iv_mtr_webhook_spec.rb @@ -3,8 +3,8 @@ require "rails_helper" RSpec.describe "USPS IV-MTR webhook", type: :request do + let(:password) { "iv_mtr_spec_password" } let(:headers) do - password = Rails.application.credentials.dig(:usps, :iv_mtr, :webhook_password) { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => ActionController::HttpAuthentication::Basic.encode_credentials("my_best_friend_the_informed_visibility_robot", password) @@ -15,7 +15,11 @@ def ingest(raw) = post("/webhooks/usps/iv_mtr", params: raw, headers: headers) def body = JSON.parse(response.body) - before { allow(USPS::IVMTR::ImportEventsJob).to receive(:perform_later) } + before do + allow(USPS::IVMTR::ImportEventsJob).to receive(:perform_later) + allow(Rails.application.credentials).to receive(:dig).and_call_original + allow(Rails.application.credentials).to receive(:dig).with(:usps, :iv_mtr, :webhook_password).and_return(password) + end it "accepts a real batch" do expect { From a8fbc12cb56bb8a39e5d4e99691a1af8cddcbe13 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Fri, 30 Oct 2026 16:52:44 +0000 Subject: [PATCH 6/6] rubocop autocorrect, refresh stale brakeman ignore fingerprint Co-Authored-By: Claude Opus 5.5 --- .../api/v1/letter_queues_controller.rb | 1 - app/controllers/public_ids_controller.rb | 2 +- .../warehouse/orders_controller.rb | 1 - app/middleware/maintenance_mode.rb | 4 +- app/views/billing/index.rb | 2 +- config/brakeman.ignore | 2 +- spec/requests/api/v1/tags_spec.rb | 26 +++---- spec/requests/api/v1/warehouse_orders_spec.rb | 2 +- spec/requests/edge_cases_spec.rb | 20 ++--- spec/requests/letters_spec.rb | 2 +- spec/requests/mcp_toolboxes_spec.rb | 76 +++++++++---------- spec/requests/warehouse_batches_spec.rb | 2 +- spec/requests/warehouse_orders_spec.rb | 12 ++- 13 files changed, 74 insertions(+), 78 deletions(-) diff --git a/app/controllers/api/v1/letter_queues_controller.rb b/app/controllers/api/v1/letter_queues_controller.rb index 5c7decee..a0612399 100644 --- a/app/controllers/api/v1/letter_queues_controller.rb +++ b/app/controllers/api/v1/letter_queues_controller.rb @@ -43,7 +43,6 @@ def create_letter letter_params.except(:address).merge(user: current_user), ) render :create_letter, status: :created - end def create_instant_letter diff --git a/app/controllers/public_ids_controller.rb b/app/controllers/public_ids_controller.rb index 38a8c3ec..ca5ad9d6 100644 --- a/app/controllers/public_ids_controller.rb +++ b/app/controllers/public_ids_controller.rb @@ -50,7 +50,7 @@ def lookup # LSV fallback for tracking numbers (Airtable, not in PublicIdResolver) if params[:id].match?(/\A[A-Z0-9]{10,}\z/i) - escaped_id = params[:id].gsub('\\') { '\\\\' }.gsub("'") { "\\'" } + escaped_id = params[:id].gsub("\\") { "\\\\" }.gsub("'") { "\\'" } lsv = LSV::MarketingShipmentRequest.first_where("{Warehouse–Tracking Number} = '#{escaped_id}'") return redirect_to show_lsv_path(LSV.slug_for(lsv), lsv.id) if lsv end diff --git a/app/controllers/warehouse/orders_controller.rb b/app/controllers/warehouse/orders_controller.rb index 67a276ce..4745ceec 100644 --- a/app/controllers/warehouse/orders_controller.rb +++ b/app/controllers/warehouse/orders_controller.rb @@ -183,5 +183,4 @@ def warehouse_order_params address_attributes: %i[first_name last_name line_1 line_2 city state postal_code country phone_number email], ) end - end diff --git a/app/middleware/maintenance_mode.rb b/app/middleware/maintenance_mode.rb index 01e962d8..5623304f 100644 --- a/app/middleware/maintenance_mode.rb +++ b/app/middleware/maintenance_mode.rb @@ -18,10 +18,10 @@ def call(env) if env["PATH_INFO"].start_with?("/api/") body = { error: "service_unavailable", message: "Theseus is down for planned maintenance." } body[:message] += " Check the thread in #hq-hq for updates." unless env["PATH_INFO"].start_with?("/api/public/") - return [503, { "content-type" => "application/json", "retry-after" => "300" }, [body.to_json]] + return [ 503, { "content-type" => "application/json", "retry-after" => "300" }, [ body.to_json ] ] end html = env["PATH_INFO"].start_with?("/back_office") ? BACK_OFFICE_HTML : PUBLIC_HTML - [503, { "content-type" => "text/html", "retry-after" => "300" }, [html]] + [ 503, { "content-type" => "text/html", "retry-after" => "300" }, [ html ] ] end end diff --git a/app/views/billing/index.rb b/app/views/billing/index.rb index aeda25fa..6f78f2fa 100644 --- a/app/views/billing/index.rb +++ b/app/views/billing/index.rb @@ -112,7 +112,7 @@ def billing_profiles_table active = @billing_profiles.select { |p| profile_has_activity?(p) } inactive_count = @billing_profiles.size - active.size - profiles_to_show = @show_all_profiles ? @billing_profiles.sort_by { |p| p.organization_name.downcase } : active.sort_by { |p| [-p.hcb_transfers.where(state: [ :unknown, :failed ]).count, p.organization_name.downcase] } + profiles_to_show = @show_all_profiles ? @billing_profiles.sort_by { |p| p.organization_name.downcase } : active.sort_by { |p| [ -p.hcb_transfers.where(state: [ :unknown, :failed ]).count, p.organization_name.downcase ] } section(class: "mb-1h") do div(class: "toolbar mb-half") do diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 754aefe8..83f1724d 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -9,7 +9,7 @@ "note": "admin-only inspector; awesome_print output passed through sanitize with an explicit tag/attribute allowlist" }, { - "fingerprint": "c3445cfef0c4723db5eabf5877e65e4c64a37158e3413a5fe96bad689c3c61b7", + "fingerprint": "5ebf484f7d05aa2b6ec7b4e78af7beade81858b7924e28897b8e8c3abe89fff8", "note": "tracking_url is built by Tracking.tracking_url_for from a fixed carrier template, not from user input" } ], diff --git a/spec/requests/api/v1/tags_spec.rb b/spec/requests/api/v1/tags_spec.rb index 223e42ed..d42b679c 100644 --- a/spec/requests/api/v1/tags_spec.rb +++ b/spec/requests/api/v1/tags_spec.rb @@ -25,10 +25,10 @@ def body = JSON.parse(response.body) it "returns tags from common tags, letters, and warehouse orders" do # Create a common tag CommonTag.create!(tag: "urgent") - + # Create a letter with a tag - create(:letter, user: user, tags: ["billing"]) - + create(:letter, user: user, tags: [ "billing" ]) + get "/api/v1/tags", headers: headers expect(response).to have_http_status(:ok) @@ -47,8 +47,8 @@ def body = JSON.parse(response.body) describe "GET /api/v1/tags/:id" do it "returns tag statistics for a tag with letters" do CommonTag.create!(tag: "marketing") - letter = create(:letter, user: user, tags: ["marketing"], postage: 0.58, aasm_state: "mailed") - + letter = create(:letter, user: user, tags: [ "marketing" ], postage: 0.58, aasm_state: "mailed") + get "/api/v1/tags/marketing", headers: headers expect(response).to have_http_status(:ok) @@ -63,12 +63,12 @@ def body = JSON.parse(response.body) sku = create(:warehouse_sku, declared_unit_cost_override: 2.0, actual_cost_to_hc: 1.0) template = Warehouse::Template.create!(name: "Test", user: user, public: true) .tap { |t| t.line_items.create!(sku: sku, quantity: 1) } - + profile = create(:billing_profile, user: user) order = Warehouse::Order.from_template( template, user: user, recipient_email: "test@example.com", address: create(:address, country: "US"), billing_profile: profile - ).tap { |o| o.save!; o.update_columns(tags: ["fulfillment"], aasm_state: "dispatched") } + ).tap { |o| o.save!; o.update_columns(tags: [ "fulfillment" ], aasm_state: "dispatched") } get "/api/v1/tags/fulfillment", headers: headers @@ -90,7 +90,7 @@ def body = JSON.parse(response.body) it "requires authentication" do CommonTag.create!(tag: "test") - + get "/api/v1/tags/test" expect(response).not_to have_http_status(:ok) @@ -100,20 +100,20 @@ def body = JSON.parse(response.body) describe "GET /api/v1/tags/:id/letters" do it "only returns the key owner's letters for a tag" do other_user = create(:user) - - mine = create(:letter, user: user, tags: ["archived"]) - create(:letter, user: other_user, tags: ["archived"]) + + mine = create(:letter, user: user, tags: [ "archived" ]) + create(:letter, user: other_user, tags: [ "archived" ]) get "/api/v1/tags/archived/letters", headers: headers expect(response).to have_http_status(:ok) expect(body).to have_key("letters") - expect(body["letters"].map { |l| l["id"] }).to eq([mine.public_id]) + expect(body["letters"].map { |l| l["id"] }).to eq([ mine.public_id ]) end it "returns empty letters for a tag with no items visible to the user" do other_user = create(:user) - create(:letter, user: other_user, tags: ["secret"]) + create(:letter, user: other_user, tags: [ "secret" ]) get "/api/v1/tags/secret/letters", headers: headers diff --git a/spec/requests/api/v1/warehouse_orders_spec.rb b/spec/requests/api/v1/warehouse_orders_spec.rb index 77615d84..f9ff7ef4 100644 --- a/spec/requests/api/v1/warehouse_orders_spec.rb +++ b/spec/requests/api/v1/warehouse_orders_spec.rb @@ -114,7 +114,7 @@ def payload(idempotency_key: nil, **overrides) expect(response).to have_http_status(:ok) expect(body).to have_key("warehouse_orders") expect(body["warehouse_orders"].count).to eq(2) - expect(body["warehouse_orders"].map { |o| o["id"] }).to match_array([order1.hc_id, order2.hc_id]) + expect(body["warehouse_orders"].map { |o| o["id"] }).to match_array([ order1.hc_id, order2.hc_id ]) end it "respects API key scoping — only shows the key owner's orders" do diff --git a/spec/requests/edge_cases_spec.rb b/spec/requests/edge_cases_spec.rb index 09cdfbc3..a9a2c5db 100644 --- a/spec/requests/edge_cases_spec.rb +++ b/spec/requests/edge_cases_spec.rb @@ -123,7 +123,7 @@ } # Should fail validation or redirect on error - expect([302, 400, 422]).to include(response.status) + expect([ 302, 400, 422 ]).to include(response.status) end it "handles CSV with bad UTF-8 encoding gracefully" do @@ -154,7 +154,7 @@ # Should not return 500 error expect(response.status).not_to eq(500) # Should be either validation error (400/422) or redirect (302) - expect([302, 400, 422]).to include(response.status) + expect([ 302, 400, 422 ]).to include(response.status) end it "accepts valid CSV file upload to letter batch" do @@ -183,7 +183,7 @@ } # Should redirect or show form, not crash (500) - expect([302, 400, 422]).to include(response.status) + expect([ 302, 400, 422 ]).to include(response.status) end end @@ -205,7 +205,7 @@ } # Should fail validation or redirect on error - expect([302, 400, 422]).to include(response.status) + expect([ 302, 400, 422 ]).to include(response.status) end it "handles warehouse CSV with bad UTF-8 encoding gracefully" do @@ -231,7 +231,7 @@ # Should not return 500 error expect(response.status).not_to eq(500) # Should be either validation error (400/422) or redirect (302) - expect([302, 400, 422]).to include(response.status) + expect([ 302, 400, 422 ]).to include(response.status) end end @@ -240,13 +240,13 @@ get letter_path(id: "nonexistent") # Should handle gracefully (either 404 or redirect) - expect([302, 404]).to include(response.status) + expect([ 302, 404 ]).to include(response.status) end it "does not crash when accessing invalid batch ID" do get letter_batch_path(id: "nonexistent") - expect([302, 404]).to include(response.status) + expect([ 302, 404 ]).to include(response.status) end it "returns error when submitting batch with invalid return address" do @@ -259,7 +259,7 @@ } } - expect([302, 422]).to include(response.status) + expect([ 302, 422 ]).to include(response.status) # Error should be present in response expect(response.body).to include("error") if response.status == 422 end @@ -272,7 +272,7 @@ get "/letters/#{letter.id}" # Should render or redirect, not crash (500) - expect([200, 302, 403, 404]).to include(response.status) + expect([ 200, 302, 403, 404 ]).to include(response.status) end it "renders public package page without crashing" do @@ -291,7 +291,7 @@ get "/packages/#{order.id}" # Should render or redirect, not crash (500) - expect([200, 302, 403, 404]).to include(response.status) + expect([ 200, 302, 403, 404 ]).to include(response.status) end end end diff --git a/spec/requests/letters_spec.rb b/spec/requests/letters_spec.rb index 190b7c9b..9ad99c81 100644 --- a/spec/requests/letters_spec.rb +++ b/spec/requests/letters_spec.rb @@ -254,7 +254,7 @@ def purchase_indicium! delete letter_path(standalone) - expect(response.status).to be_in([302, 303]) + expect(response.status).to be_in([ 302, 303 ]) expect { Letter.find(letter_id) }.to raise_error(ActiveRecord::RecordNotFound) end end diff --git a/spec/requests/mcp_toolboxes_spec.rb b/spec/requests/mcp_toolboxes_spec.rb index 4367bab9..be54d756 100644 --- a/spec/requests/mcp_toolboxes_spec.rb +++ b/spec/requests/mcp_toolboxes_spec.rb @@ -147,7 +147,7 @@ def expect_success(response) it "returns the current user's identity and permissions" do response = expect_success(dispatch("theseus_whoami", {})) data = JSON.parse(text_of(response)) - + expect(data["id"]).to eq(user.id) expect(data["username"]).to eq(user.username) expect(data["email"]).to eq(user.email) @@ -161,7 +161,7 @@ def expect_success(response) letter = create(:letter, user: user) response = expect_success(dispatch("theseus_lookup", { "id" => letter.public_id })) data = JSON.parse(text_of(response)) - + expect(data["type"]).to eq("Letter") expect(data["id"]).to eq(letter.public_id) end @@ -175,10 +175,10 @@ def expect_success(response) user: user, recipient_email: "test@example.com", address: create(:address), billing_profile: profile ).tap(&:save!) - + response = expect_success(dispatch("theseus_lookup", { "id" => order.public_id })) data = JSON.parse(text_of(response)) - + expect(data["type"]).to eq("Warehouse::Order") expect(data["id"]).to eq(order.hc_id) end @@ -193,16 +193,16 @@ def expect_success(response) user: user, recipient_email: "test@example.com", address: create(:address), billing_profile: profile ).tap { |o| o.update!(tracking_number: tracking); o.save! } - + response = expect_success(dispatch("theseus_lookup", { "id" => tracking })) data = JSON.parse(text_of(response)) - + expect(data["tracking"]).to eq(tracking) end it "returns error when ID is not found" do response = dispatch("theseus_lookup", { "id" => "totally_bogus_id_xyz" }) - + expect(response[:isError]).to be_truthy expect(text_of(response)).to include("Nothing found") end @@ -218,16 +218,16 @@ def expect_success(response) scopes: Toolchest.configuration.scopes.keys, token: nil ) - + my_letter = create(:letter, user: non_admin_user) other_letter = create(:letter) # Created by admin - + response = Toolchest::Current.set(auth: non_admin_auth) do Toolchest.router.dispatch("letters_search", {}) end response = expect_success(response) data = JSON.parse(text_of(response)) - + letter_ids = data["letters"].map { |l| l["id"] } expect(letter_ids).to include(my_letter.public_id) expect(letter_ids).not_to include(other_letter.public_id) @@ -236,10 +236,10 @@ def expect_success(response) it "filters by status" do pending_letter = create(:letter, user: user, aasm_state: "pending") printed_letter = create(:letter, user: user, aasm_state: "printed") - + response = expect_success(dispatch("letters_search", { "status" => "pending" })) data = JSON.parse(text_of(response)) - + letter_ids = data["letters"].map { |l| l["id"] } expect(letter_ids).to include(pending_letter.public_id) expect(letter_ids).not_to include(printed_letter.public_id) @@ -256,17 +256,17 @@ def expect_success(response) scopes: Toolchest.configuration.scopes.keys, token: nil ) - + profile = create(:billing_profile, user: non_admin_user) sku = create(:warehouse_sku) template = Warehouse::Template.create!(name: "Test", user: non_admin_user, public: true) .tap { |t| t.line_items.create!(sku: sku, quantity: 1) } - + my_order = Warehouse::Order.from_template( template, user: non_admin_user, recipient_email: "test@example.com", address: create(:address), billing_profile: profile ).tap(&:save!) - + other_user = create(:user, can_warehouse: true) other_profile = create(:billing_profile, user: other_user) other_template = Warehouse::Template.create!(name: "Other", user: other_user, public: true) @@ -275,13 +275,13 @@ def expect_success(response) other_template, user: other_user, recipient_email: "other@example.com", address: create(:address), billing_profile: other_profile ).tap(&:save!) - + response = Toolchest::Current.set(auth: non_admin_auth) do Toolchest.router.dispatch("warehouse_orders_search", {}) end response = expect_success(response) data = JSON.parse(text_of(response)) - + order_ids = data["orders"].map { |o| o["id"] } expect(order_ids).to include(my_order.hc_id) expect(order_ids).not_to include(other_order.hc_id) @@ -292,20 +292,20 @@ def expect_success(response) sku = create(:warehouse_sku) template = Warehouse::Template.create!(name: "Test", user: user, public: true) .tap { |t| t.line_items.create!(sku: sku, quantity: 1) } - + draft_order = Warehouse::Order.from_template( template, user: user, recipient_email: "test@example.com", address: create(:address), billing_profile: profile, aasm_state: "draft" ).tap(&:save!) - + dispatched_order = Warehouse::Order.from_template( template, user: user, recipient_email: "test2@example.com", address: create(:address), billing_profile: profile, aasm_state: "dispatched" ).tap(&:save!) - + response = expect_success(dispatch("warehouse_orders_search", { "state" => "draft" })) data = JSON.parse(text_of(response)) - + order_ids = data["orders"].map { |o| o["id"] } expect(order_ids).to include(draft_order.hc_id) expect(order_ids).not_to include(dispatched_order.hc_id) @@ -324,10 +324,10 @@ def expect_success(response) template, user: user, recipient_email: "test@example.com", address: create(:address), billing_profile: profile ).tap(&:save!) - + response = expect_success(dispatch("warehouse_orders_show", { "order_id" => order.hc_id })) data = JSON.parse(text_of(response)) - + expect(data["id"]).to eq(order.hc_id) expect(data["line_items"]).to be_an(Array) expect(data["line_items"].length).to be > 0 @@ -343,7 +343,7 @@ def expect_success(response) template, user: other_user, recipient_email: "test@example.com", address: create(:address), billing_profile: other_profile ).tap(&:save!) - + # Switch to different non-admin user non_admin = create(:user) non_admin_auth = Toolchest::AuthContext.new( @@ -351,11 +351,11 @@ def expect_success(response) scopes: Toolchest.configuration.scopes.keys, token: nil ) - + response = Toolchest::Current.set(auth: non_admin_auth) do Toolchest.router.dispatch("warehouse_orders_show", { "order_id" => other_order.hc_id }) end - + expect(response[:isError]).to be_truthy end end @@ -368,17 +368,17 @@ def expect_success(response) scopes: Toolchest.configuration.scopes.keys, token: nil ) - + user_ra = create(:return_address, user: non_admin, shared: false) shared_ra = create(:return_address, user: create(:user), shared: true) other_private_ra = create(:return_address, user: create(:user), shared: false) - + response = Toolchest::Current.set(auth: non_admin_auth) do Toolchest.router.dispatch("return_addresses_list", {}) end response = expect_success(response) data = JSON.parse(text_of(response)) - + ra_ids = data["return_addresses"].map { |r| r["id"] } expect(ra_ids).to include(user_ra.id) expect(ra_ids).to include(shared_ra.id) @@ -389,10 +389,10 @@ def expect_success(response) user_ra = create(:return_address, user: user, shared: false) shared_ra = create(:return_address, user: create(:user), shared: true) other_private_ra = create(:return_address, user: create(:user), shared: false) - + response = expect_success(dispatch("return_addresses_list", {})) data = JSON.parse(text_of(response)) - + ra_ids = data["return_addresses"].map { |r| r["id"] } expect(ra_ids).to include(user_ra.id) expect(ra_ids).to include(shared_ra.id) @@ -403,10 +403,10 @@ def expect_success(response) describe "return_addresses show" do it "returns the address details for owned addresses" do ra = create(:return_address, user: user) - + response = expect_success(dispatch("return_addresses_show", { "return_address_id" => ra.id })) data = JSON.parse(text_of(response)) - + expect(data["id"]).to eq(ra.id) expect(data["name"]).to eq(ra.name) end @@ -414,28 +414,28 @@ def expect_success(response) it "returns shared addresses to all users" do other_user = create(:user) shared_ra = create(:return_address, user: other_user, shared: true) - + response = expect_success(dispatch("return_addresses_show", { "return_address_id" => shared_ra.id })) data = JSON.parse(text_of(response)) - + expect(data["id"]).to eq(shared_ra.id) end it "prevents IDOR - blocks access to other users' private addresses" do other_user = create(:user) private_ra = create(:return_address, user: other_user, shared: false) - + non_admin = create(:user) restricted_auth = Toolchest::AuthContext.new( resource_owner: non_admin, scopes: Toolchest.configuration.scopes.keys, token: nil ) - + response = Toolchest::Current.set(auth: restricted_auth) do Toolchest.router.dispatch("return_addresses_show", { "return_address_id" => private_ra.id }) end - + expect(response[:isError]).to be_truthy end end diff --git a/spec/requests/warehouse_batches_spec.rb b/spec/requests/warehouse_batches_spec.rb index 5bbe0ec5..5ae2a6de 100644 --- a/spec/requests/warehouse_batches_spec.rb +++ b/spec/requests/warehouse_batches_spec.rb @@ -249,7 +249,7 @@ def upload(rows) it "does not show private templates you don't own" do private_other = Warehouse::Template.create!(name: "Other User's Private", user: create(:user), public: false) - + get new_warehouse_batch_path expect(response.body).not_to include("Other User's Private") end diff --git a/spec/requests/warehouse_orders_spec.rb b/spec/requests/warehouse_orders_spec.rb index c8f3bd15..10dbb95c 100644 --- a/spec/requests/warehouse_orders_spec.rb +++ b/spec/requests/warehouse_orders_spec.rb @@ -85,14 +85,14 @@ it "POST create with address, line items, and tags saves as draft" do sku2 = create(:warehouse_sku, name: "Patches") - + post warehouse_orders_path, params: { warehouse_order: { user_facing_title: "Custom order", user_facing_description: "A test order", recipient_email: "customer@example.com", notify_on_dispatch: "1", - tags: ["urgent", "custom"], + tags: [ "urgent", "custom" ], address_attributes: { first_name: "Jane", last_name: "Doe", @@ -174,10 +174,10 @@ describe "order show page" do it "GET show renders the order with line items table and status badge" do order.line_items.create!(sku: create(:warehouse_sku, name: "Extra SKU"), quantity: 3) - + get warehouse_order_path(order) expect(response).to have_http_status(:ok) - + # Check for line items - SKU names expect(response.body).to include(sku.name) expect(response.body).to include("Extra SKU") @@ -187,7 +187,7 @@ describe "order index" do let(:order2) do Warehouse::Order.from_template( - template, user: user, recipient_email: "b@c.com", + template, user: user, recipient_email: "b@c.com", address: create(:address, country: "US"), billing_profile: profile ).tap(&:save!) end @@ -224,8 +224,6 @@ expect(response).to have_http_status(:ok) # Just verify the page renders without error end - - end describe "sending to warehouse" do