From f950491a8a8a8687298bad2f83c07325f7f0d8ca Mon Sep 17 00:00:00 2001 From: Adrian Curtin <48138055+AdrianCurtin@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:13:16 -0400 Subject: [PATCH 1/5] Add Range support to between constraint The `.between` constraint now accepts Ruby Range objects in addition to 2-element arrays, providing a more idiomatic API for range queries. Inclusive ranges (`..`) map to `$lte` for the upper bound, while exclusive ranges (`...`) map to `$lt`. Beginless (`..end`) and endless (`begin..`) ranges are fully supported. All changes are backwards compatible with existing array-based queries. Includes comprehensive unit and integration tests, and snapshot tests for query compilation. --- Gemfile.lock | 2 +- lib/parse/query/constraints.rb | 39 ++++++- lib/parse/stack/version.rb | 2 +- .../between_constraint_integration_test.rb | 106 ++++++++++++++++++ .../parse/query/constraints/between_test.rb | 95 ++++++++++++++++ test/lib/parse/query_compile_snapshot_test.rb | 13 +++ .../between_range_exclusive.json | 8 ++ .../between_range_inclusive.json | 8 ++ 8 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 test/lib/parse/query/constraints/between_test.rb create mode 100644 test/snapshots/query_compile/between_range_exclusive.json create mode 100644 test/snapshots/query_compile/between_range_inclusive.json diff --git a/Gemfile.lock b/Gemfile.lock index 2c82b08..5ada037 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - parse-stack-next (5.7.1) + parse-stack-next (5.7.2) activemodel (>= 6.1, < 9) activesupport (>= 6.1, < 9) connection_pool (>= 2.2, < 4) diff --git a/lib/parse/query/constraints.rb b/lib/parse/query/constraints.rb index 9f9763c..52b67c4 100644 --- a/lib/parse/query/constraints.rb +++ b/lib/parse/query/constraints.rb @@ -2516,6 +2516,19 @@ def build # User.where(:name.between => ["Alice", "John"]) # # Generates: "name": { "$gte": "Alice", "$lte": "John" } # + # # A Ruby Range works the same way as a 2-element array. An inclusive + # # range (`..`) maps its end to $lte, while an exclusive range (`...`) + # # maps its end to $lt. + # User.where(:age.between => 18..65) + # # Generates: "age": { "$gte": 18, "$lte": 65 } + # + # Record.where(:date.between => 5.days.ago...2.days.ago) + # # Generates: "date": { "$gte": <5 days ago>, "$lt": <2 days ago> } + # + # # Beginless/endless ranges only constrain the side that is present. + # User.where(:age.between => 18..) + # # Generates: "age": { "$gte": 18 } + # class BetweenConstraint < Constraint # @!method between # A registered method on a symbol to create the constraint. @@ -2526,9 +2539,11 @@ class BetweenConstraint < Constraint # @return [Hash] the compiled constraint. def build + return build_range(@value) if @value.is_a?(Range) + value = formatted_value unless value.is_a?(Array) && value.length == 2 - raise ArgumentError, "#{self.class}: Value must be an array with exactly 2 elements [min_value, max_value]" + raise ArgumentError, "#{self.class}: Value must be an array with exactly 2 elements [min_value, max_value], or a Range" end min_value, max_value = value @@ -2542,6 +2557,28 @@ def build Parse::Constraint::LessThanOrEqualConstraint.key => formatted_max, } } end + + private + + # @return [Hash] the compiled constraint for a Ruby Range value. + def build_range(range) + bounds = {} + + unless range.begin.nil? + bounds[Parse::Constraint::GreaterThanOrEqualConstraint.key] = Parse::Constraint.formatted_value(range.begin) + end + + unless range.end.nil? + upper_key = range.exclude_end? ? Parse::Constraint::LessThanConstraint.key : Parse::Constraint::LessThanOrEqualConstraint.key + bounds[upper_key] = Parse::Constraint.formatted_value(range.end) + end + + if bounds.empty? + raise ArgumentError, "#{self.class}: Range must have a begin, an end, or both (ex. 5.., ..25, 5..25)" + end + + { @operation.operand => bounds } + end end # @!visibility private diff --git a/lib/parse/stack/version.rb b/lib/parse/stack/version.rb index 735f6fb..7dc9829 100644 --- a/lib/parse/stack/version.rb +++ b/lib/parse/stack/version.rb @@ -6,6 +6,6 @@ module Parse # The Parse Server SDK for Ruby module Stack # The current version. - VERSION = "5.7.1" + VERSION = "5.7.2" end end diff --git a/test/lib/parse/between_constraint_integration_test.rb b/test/lib/parse/between_constraint_integration_test.rb index a88dc4f..0ddf961 100644 --- a/test/lib/parse/between_constraint_integration_test.rb +++ b/test/lib/parse/between_constraint_integration_test.rb @@ -401,6 +401,112 @@ def test_between_constraint_with_strings end end + def test_between_constraint_with_range + skip "Docker integration tests require PARSE_TEST_USE_DOCKER=true" unless ENV["PARSE_TEST_USE_DOCKER"] == "true" + + with_parse_server do + with_timeout(15, "between constraint with Range test") do + puts "\n=== Testing Between Constraint with Ruby Range ===" + + 5.times do |i| + user = BetweenTestUser.new(name: "User #{i}", age: 20 + i * 5, score: 70 + i * 5) + assert user.save, "User #{i} should save" + end + + # Inclusive range (..) should behave like [min, max] + inclusive_users = BetweenTestUser.query + .where(:age.between => 25..35) + .order(:age.asc) + .results + + assert_equal 3, inclusive_users.length, "Should find 3 users in inclusive age range" + assert_equal [25, 30, 35], inclusive_users.map(&:age), "Should include both boundary ages" + + # Exclusive range (...) should exclude the upper bound + exclusive_users = BetweenTestUser.query + .where(:age.between => 25...35) + .order(:age.asc) + .results + + assert_equal 2, exclusive_users.length, "Should find 2 users in exclusive age range" + assert_equal [25, 30], exclusive_users.map(&:age), "Should exclude the upper boundary age" + + # Array and Range forms should produce identical results + array_users = BetweenTestUser.query + .where(:age.between => [25, 35]) + .order(:age.asc) + .results + + assert_equal inclusive_users.map(&:id), array_users.map(&:id), "Array and inclusive Range forms should match" + + puts "✅ Between constraint with Ruby Range works correctly" + end + end + end + + def test_between_constraint_with_endless_and_beginless_range + skip "Docker integration tests require PARSE_TEST_USE_DOCKER=true" unless ENV["PARSE_TEST_USE_DOCKER"] == "true" + + with_parse_server do + with_timeout(15, "between constraint with endless/beginless Range test") do + puts "\n=== Testing Between Constraint with Endless/Beginless Range ===" + + 5.times do |i| + user = BetweenTestUser.new(name: "User #{i}", age: 20 + i * 5, score: 70 + i * 5) + assert user.save, "User #{i} should save" + end + + # Endless range (5..) should only apply the lower bound + older_users = BetweenTestUser.query + .where(:age.between => 30..) + .order(:age.asc) + .results + + assert_equal 3, older_users.length, "Should find users 30 and older" + assert_equal [30, 35, 40], older_users.map(&:age) + + # Beginless range (..25) should only apply the upper bound + younger_users = BetweenTestUser.query + .where(:age.between => ..25) + .order(:age.asc) + .results + + assert_equal 2, younger_users.length, "Should find users 25 and younger" + assert_equal [20, 25], younger_users.map(&:age) + + puts "✅ Between constraint with endless/beginless Range works correctly" + end + end + end + + def test_between_constraint_with_time_range + skip "Docker integration tests require PARSE_TEST_USE_DOCKER=true" unless ENV["PARSE_TEST_USE_DOCKER"] == "true" + + with_parse_server do + with_timeout(15, "between constraint with Time Range test") do + puts "\n=== Testing Between Constraint with Time Range ===" + + old_user = BetweenTestUser.new(name: "Old User", age: 45, join_date: Date.parse("2020-01-15"), score: 100) + assert old_user.save, "Old user should save" + + recent_user = BetweenTestUser.new(name: "Recent User", age: 28, join_date: Date.parse("2023-06-10"), score: 85) + assert recent_user.save, "Recent user should save" + + new_user = BetweenTestUser.new(name: "New User", age: 26, join_date: Date.parse("2024-08-01"), score: 75) + assert new_user.save, "New user should save" + + users_2023 = BetweenTestUser.query + .where(:join_date.between => Date.parse("2023-01-01")..Date.parse("2023-12-31")) + .results + + assert_equal 1, users_2023.length, "Should find 1 user who joined in 2023" + assert_equal "Recent User", users_2023.first.name + + puts "✅ Between constraint with Time Range works correctly" + end + end + end + def test_between_constraint_string_vs_manual_comparison skip "Docker integration tests require PARSE_TEST_USE_DOCKER=true" unless ENV["PARSE_TEST_USE_DOCKER"] == "true" diff --git a/test/lib/parse/query/constraints/between_test.rb b/test/lib/parse/query/constraints/between_test.rb new file mode 100644 index 0000000..ed368a0 --- /dev/null +++ b/test/lib/parse/query/constraints/between_test.rb @@ -0,0 +1,95 @@ +require_relative "../../../../test_helper" + +class TestBetweenConstraint < Minitest::Test + extend Minitest::Spec::DSL + include ConstraintTests + + def setup + @klass = Parse::Constraint::BetweenConstraint + @key = nil # This constraint doesn't map to a single key + @operand = :between + @keys = [:between] + @skip_scalar_values_test = true + end + + def build(value) + if value.is_a?(Array) && value.length == 2 + min_value, max_value = value + { "field" => { + "$gte" => Parse::Constraint.formatted_value(min_value), + "$lte" => Parse::Constraint.formatted_value(max_value), + } } + else + { "field" => Parse::Constraint.formatted_value(value) } + end + end + + def test_with_numeric_array + constraint = @klass.new(:age, [5, 25]) + expected = { age: { :$gte => 5, :$lte => 25 } } + assert_equal expected, constraint.build + end + + def test_with_inclusive_range + constraint = @klass.new(:age, 5..25) + expected = { age: { :$gte => 5, :$lte => 25 } } + assert_equal expected, constraint.build + end + + def test_with_exclusive_range + constraint = @klass.new(:age, 5...25) + expected = { age: { :$gte => 5, :$lt => 25 } } + assert_equal expected, constraint.build + end + + def test_with_date_range + start_date = DateTime.new(2023, 1, 1) + end_date = DateTime.new(2023, 12, 31) + constraint = @klass.new(:created_at, start_date...end_date) + + expected_start = { __type: "Date", iso: start_date.utc.iso8601(3) } + expected_end = { __type: "Date", iso: end_date.utc.iso8601(3) } + expected = { created_at: { :$gte => expected_start, :$lt => expected_end } } + + assert_equal expected, constraint.build + end + + def test_with_beginless_range + constraint = @klass.new(:age, ..25) + expected = { age: { :$lte => 25 } } + assert_equal expected, constraint.build + end + + def test_with_endless_range + constraint = @klass.new(:age, 5..) + expected = { age: { :$gte => 5 } } + assert_equal expected, constraint.build + end + + def test_with_endless_exclusive_range + constraint = @klass.new(:age, 5...) + expected = { age: { :$gte => 5 } } + assert_equal expected, constraint.build + end + + def test_invalid_single_value_raises_error + constraint = @klass.new(:age, 25) + assert_raises(ArgumentError) do + constraint.build + end + end + + def test_invalid_one_element_array_raises_error + constraint = @klass.new(:age, [25]) + assert_raises(ArgumentError) do + constraint.build + end + end + + def test_invalid_three_element_array_raises_error + constraint = @klass.new(:age, [5, 15, 25]) + assert_raises(ArgumentError) do + constraint.build + end + end +end diff --git a/test/lib/parse/query_compile_snapshot_test.rb b/test/lib/parse/query_compile_snapshot_test.rb index 448c523..2645053 100644 --- a/test/lib/parse/query_compile_snapshot_test.rb +++ b/test/lib/parse/query_compile_snapshot_test.rb @@ -149,6 +149,19 @@ def test_exists_constraint assert_snapshot(compile(q), name: "exists_constraint", group: GROUP) end + def test_between_range_constraint + # An inclusive Range (`..`) on `.between` compiles identically to the + # 2-element Array form: $gte for the start, $lte for the end. + q = SnapPost.where(:likes.between => 10..100) + assert_snapshot(compile(q), name: "between_range_inclusive", group: GROUP) + end + + def test_between_exclusive_range_constraint + # An exclusive Range (`...`) maps its end to $lt instead of $lte. + q = SnapPost.where(:likes.between => 10...100) + assert_snapshot(compile(q), name: "between_range_exclusive", group: GROUP) + end + def test_tags_all_array_constraint # `:tags.all => [...]` compiles to the `$all` REST operator (not a # pipeline). Snapshot under normalized array order — $all is set-semantic. diff --git a/test/snapshots/query_compile/between_range_exclusive.json b/test/snapshots/query_compile/between_range_exclusive.json new file mode 100644 index 0000000..581da8c --- /dev/null +++ b/test/snapshots/query_compile/between_range_exclusive.json @@ -0,0 +1,8 @@ +{ + "where": { + "likes": { + "$gte": 10, + "$lt": 100 + } + } +} diff --git a/test/snapshots/query_compile/between_range_inclusive.json b/test/snapshots/query_compile/between_range_inclusive.json new file mode 100644 index 0000000..f18f16f --- /dev/null +++ b/test/snapshots/query_compile/between_range_inclusive.json @@ -0,0 +1,8 @@ +{ + "where": { + "likes": { + "$gte": 10, + "$lte": 100 + } + } +} From a76e79a444e6587d627b1b604f8bba2e5e7e4939 Mon Sep 17 00:00:00 2001 From: Adrian Curtin <48138055+AdrianCurtin@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:32:23 -0400 Subject: [PATCH 2/5] Use presigned URLs in image embeddings Fix `embed_image` to forward the file's presigned URL to embedding providers and downloaders instead of the bare canonical URL. This resolves 403 errors when using private-bucket file adapters (S3/GCS with `presignedUrl: true`), where the canonical `file.url` lacks the required signature. Falls back to the bare URL when no valid presigned URL is present. The stored digest remains keyed on the canonical URL to avoid re-embedding on signature rotation. --- CHANGELOG.md | 29 +++++++++++++++ lib/parse/model/core/embed_managed.rb | 34 +++++++++++++++-- test/lib/parse/embed_managed_image_test.rb | 43 ++++++++++++++++++++++ 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aea8420..65f7fd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ ## parse-stack-next Changelog +### 5.7.2 + +#### `between` accepts Ruby Range values + +- **NEW**: The `between` constraint now accepts a Ruby `Range` in addition to + a 2-element array, so `Person.where(:age.between => 5..25)` and + `Record.where(:date.between => 2.days.ago...5.days.ago)` work directly. An + inclusive range (`..`) maps its upper bound to `$lte`, matching the existing + array form, while an exclusive range (`...`) maps it to `$lt` instead. + Beginless (`..25`) and endless (`5..`) ranges are also supported and + constrain only the side that is present, so `Person.where(:age.between => + 18..)` compiles to `{"$gte" => 18}` with no upper bound. The array form is + unchanged, and both forms produce identical output for the same bounds. + +#### `embed_image` forwards a presigned URL when the source file has one + +- **FIXED**: `embed_image` always sent the source file's bare `file.url` to + the embedding provider (or to the SDK's own `:bytes`-mode downloader). On a + private-bucket file adapter (S3/GCS configured with `presignedUrl: true`), + `file.url` is the canonical URL with its signature stripped, so the + provider's fetch (or the SDK's download) got a 403 instead of the image. + `Parse::File` already captures the signed variant in `file.presigned_url` + whenever Parse Server returns one, but `embed_image` never read it. + Recompute now forwards `file.presigned_url` when it is present and not yet + expired, and falls back to the bare URL otherwise, for both `source: :url` + and `source: :bytes`. The stored digest is still keyed on the bare + canonical URL, so a save that only rotates the file's signature does not + trigger a needless re-embed. + ### 5.7.1 #### Cache-invalidation webhooks no longer break every application hook for the same trigger diff --git a/lib/parse/model/core/embed_managed.rb b/lib/parse/model/core/embed_managed.rb index 9f3ca90..0a7287b 100644 --- a/lib/parse/model/core/embed_managed.rb +++ b/lib/parse/model/core/embed_managed.rb @@ -687,7 +687,7 @@ def self.recompute_embedding!(record, directive) return if stored_digest == digest && target_present provider = Parse::Embeddings.provider(directive.provider_name) - vectors = call_provider(provider, directive, input) + vectors = call_provider(provider, directive, input, record) unless vectors.is_a?(Array) && vectors.length == 1 && vectors.first.is_a?(Array) raise Parse::Embeddings::InvalidResponseError, "Parse::Core::EmbedManaged (#{record.class}##{directive.into}): provider " \ @@ -774,16 +774,27 @@ def self.build_source_input(record, directive) # provider a {Parse::Embeddings::ImageFetch::FetchedImage}; `:url` # mode forwards the raw URL String (the provider validates and # fetches it itself). - def self.call_provider(provider, directive, input) + # + # `input` is the bare canonical URL used for the digest (see + # {.build_source_input}). It stays stable across saves, so an + # unsigned re-read of the same file location does not force a + # re-embed. + # The actual fetch/forward target prefers the file's presigned + # URL ({Parse::File#presigned_url}) when one is currently valid, + # since a private-bucket adapter's bare `file.url` is stripped of + # its signature and will not resolve for the provider or for the + # SDK's own `:bytes`-mode download. + def self.call_provider(provider, directive, input, record) if directive.image? + fetch_url = presigned_fetch_url(record, directive, input) source = if directive.bytes_mode? Parse::Embeddings::ImageFetch.fetch!( - input, + fetch_url, allow_insecure: directive.allow_insecure ? true : false, exif_strip: directive.exif_strip != false, ) else - input + fetch_url end provider.embed_image([source], input_type: directive.input_type, @@ -793,6 +804,21 @@ def self.call_provider(provider, directive, input) end end + # @!visibility private + # Resolve the URL to actually fetch/forward for an image + # directive: the source file's currently-valid presigned URL if + # it has one, otherwise the bare canonical `fallback` (the same + # string used for the digest). Never used for text directives, so + # `directive.sources.first` is always a `:file` property here. + def self.presigned_fetch_url(record, directive, fallback) + file = record.public_send(directive.sources.first) + if file.respond_to?(:presigned_url_valid?) && file.presigned_url_valid? + file.presigned_url + else + fallback + end + end + # @!visibility private # Concatenate source-field string values. `nil` and blank entries # are skipped; remaining values are joined with a double newline. diff --git a/test/lib/parse/embed_managed_image_test.rb b/test/lib/parse/embed_managed_image_test.rb index 6bde1bf..0fae6c6 100644 --- a/test/lib/parse/embed_managed_image_test.rb +++ b/test/lib/parse/embed_managed_image_test.rb @@ -201,6 +201,49 @@ def test_recompute_re_embeds_when_file_url_changes assert_equal ["https://1.1.1.1/cover_v2.jpg"], @stub.calls.last[:sources] end + def test_recompute_forwards_presigned_url_when_valid + doc = ImageDoc.new + doc.cover_art = file_with_url("https://1.1.1.1/cover.jpg") + doc.cover_art.instance_variable_set(:@presigned_url, "https://1.1.1.1/cover.jpg?X-Amz-Signature=abc") + doc.cover_art.instance_variable_set(:@presigned_url_expires_at, Time.now.utc + 3600) + + Parse::Core::EmbedManaged.recompute_embedding!(doc, directive_for(ImageDoc, :cover_embedding)) + + assert_equal 1, @stub.calls.length + assert_equal ["https://1.1.1.1/cover.jpg?X-Amz-Signature=abc"], @stub.calls.first[:sources], + "provider should receive the presigned URL, not the bare canonical file.url" + end + + def test_recompute_falls_back_to_bare_url_when_presigned_url_expired + doc = ImageDoc.new + doc.cover_art = file_with_url("https://1.1.1.1/cover.jpg") + doc.cover_art.instance_variable_set(:@presigned_url, "https://1.1.1.1/cover.jpg?X-Amz-Signature=abc") + doc.cover_art.instance_variable_set(:@presigned_url_expires_at, Time.now.utc - 1) + + Parse::Core::EmbedManaged.recompute_embedding!(doc, directive_for(ImageDoc, :cover_embedding)) + + assert_equal ["https://1.1.1.1/cover.jpg"], @stub.calls.first[:sources], + "an expired presigned URL must not be forwarded" + end + + def test_recompute_digest_is_stable_across_presigned_url_rotation + doc = ImageDoc.new + doc.cover_art = file_with_url("https://1.1.1.1/cover.jpg") + d = directive_for(ImageDoc, :cover_embedding) + Parse::Core::EmbedManaged.recompute_embedding!(doc, d) + digest_before = doc.cover_embedding_digest + + # Same underlying file location, but Parse Server minted a fresh + # signature (as it does on every read). The digest must not change + # since it is keyed on the bare canonical URL, not the signature. + doc.cover_art.instance_variable_set(:@presigned_url, "https://1.1.1.1/cover.jpg?X-Amz-Signature=rotated") + doc.cover_art.instance_variable_set(:@presigned_url_expires_at, Time.now.utc + 3600) + Parse::Core::EmbedManaged.recompute_embedding!(doc, d) + + assert_equal digest_before, doc.cover_embedding_digest + assert_equal 1, @stub.calls.length, "Provider must not be re-called when only the signature rotated" + end + def test_recompute_clears_vector_and_digest_when_file_is_nil doc = ImageDoc.new doc.cover_art = file_with_url("https://1.1.1.1/cover.jpg") From 1acd1e8def871af9ff42a1431da41d83a39936b7 Mon Sep 17 00:00:00 2001 From: Adrian Curtin <48138055+AdrianCurtin@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:14:27 -0400 Subject: [PATCH 3/5] Fix aliased parse_class lookup and logger routing Two fixes for Query#get and _safe_warn: 1. Query#get now resolves aliased parse_class names correctly by passing the table name as a String to Parse::Object.build (which handles parse_class aliasing) instead of pre-resolving to a Class via Object.const_get, which only worked for exact constant name matches. 2. Parse::Client._safe_warn now routes warnings through Parse::Middleware::Logging.logger when configured, ensuring warnings appear in the app's configured logger (e.g., Rails.logger) instead of always going to STDERR. Falls back to STDERR when no logger is configured, preserving prior behavior. --- CHANGELOG.md | 22 +++++++++++ lib/parse/client.rb | 19 ++++++++-- lib/parse/client/request.rb | 20 ++++++++-- lib/parse/query.rb | 12 ++++-- test/lib/parse/client/safe_warn_test.rb | 47 ++++++++++++++++++++++++ test/lib/parse/query_get_test.rb | 49 +++++++++++++++++++++++++ 6 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 test/lib/parse/query_get_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 65f7fd3..735ade3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,28 @@ canonical URL, so a save that only rotates the file's signature does not trigger a needless re-embed. +#### `Query#get` now resolves aliased `parse_class` names correctly + +- **FIXED**: `Query#get` looked up the target class with a raw + `Object.const_get(@table)`, which only worked when the Parse class name + matched the Ruby constant name exactly. A model that renames its table via + `parse_class "SomeOtherName"` was never found by this lookup, so `get` + silently fell back to a generic `Parse::Object`/`Parse::Pointer` instead of + hydrating the declared model. `Query#get` now passes the table name through + to `Parse::Object.build` as a string, letting it run its own + `Parse::Model.find_class` resolution, which already understands + `parse_class` aliasing. + +#### `_safe_warn` now writes through a configured logger + +- **FIXED**: Internal warnings for authentication, timeout, and cloud-code + errors (`Parse::Client._safe_warn`) always wrote to STDERR, even when an + app had configured `Parse.logger = Rails.logger` (or any other logger) for + the rest of its Parse request/response logging. These warnings now route + through `Parse::Middleware::Logging.logger` when one is configured, so they + land in the same place as the app's other logs; STDERR remains the fallback + when no logger is configured, matching prior behavior. + ### 5.7.1 #### Cache-invalidation webhooks no longer break every application hook for the same trigger diff --git a/lib/parse/client.rb b/lib/parse/client.rb index 9aa77ad..a8a5a67 100644 --- a/lib/parse/client.rb +++ b/lib/parse/client.rb @@ -505,23 +505,34 @@ def setup(opts = {}, &block) end # @!visibility private - # Emit a redacted warning about a Parse::Response error to stderr. + # Emit a redacted warning about a Parse::Response error. # # Routes the response error string through # {Parse::Middleware::BodyBuilder.redact} to strip credentials (passwords, # tokens, sessionTokens, access_tokens, authData) before logging, and # truncates to {SAFE_WARN_MAX_ERROR_LENGTH} chars. # + # Writes through {Parse::Middleware::Logging.logger} when the app has + # configured one (`Parse.logger = ...`), so these warnings land wherever + # the rest of the app's Parse request/response logging goes instead of + # bypassing it. Falls back to plain `warn` (STDERR) when no logger is + # configured, matching prior behavior. + # # @param tag [String] the bracketed prefix (e.g. "AuthenticationError"). # @param response [Parse::Response] the response carrying the error. # @param name [String, nil] optional cloud-function or job name for context. # @return [nil] def _safe_warn(tag, response, name: nil) err = Parse::Middleware::BodyBuilder.redact(response.error.to_s)[0, SAFE_WARN_MAX_ERROR_LENGTH] - if name - warn "[Parse:#{tag}] `#{name}` [#{response.code}] #{err} (HTTP #{response.http_status})" + msg = if name + "[Parse:#{tag}] `#{name}` [#{response.code}] #{err} (HTTP #{response.http_status})" + else + "[Parse:#{tag}] [E-#{response.code}] #{response.request} : #{err} (#{response.http_status})" + end + if Parse::Middleware::Logging.logger + Parse::Middleware::Logging.logger.warn(msg) else - warn "[Parse:#{tag}] [E-#{response.code}] #{response.request} : #{err} (#{response.http_status})" + warn msg end nil end diff --git a/lib/parse/client/request.rb b/lib/parse/client/request.rb index ca442b4..89912eb 100644 --- a/lib/parse/client/request.rb +++ b/lib/parse/client/request.rb @@ -17,12 +17,24 @@ class Request # @!attribute [rw] body # @return [Hash] the body of this request. - # TODO: Document opts and cache options. - # @!attribute [rw] opts - # @return [Hash] a set of options for this request. + # @return [Hash] per-request options consumed by {Parse::Client#request} + # when it builds the HTTP headers for this request. Recognized keys: + # * `:cache` — `false` sends `Cache-Control: no-cache`; `:write_only` + # skips the cache read but still writes the response; a `Numeric` + # overrides the cache expiration (seconds) for this request only. + # * `:use_master_key` — `false` forces the master key off for this + # request even if the client has one configured. + # * `:session_token` — a session token to authenticate this request as + # a specific user, bypassing the client's default auth context. + # * `:idempotent` — explicitly enables/disables idempotency-header + # generation for this request, overriding the class-level defaults. + # * `:request_id` — a caller-supplied idempotency key; see + # {.enable_idempotency!}. # @!attribute [rw] cache - # @return [Boolean] + # @return [Boolean] unused by {Parse::Request} itself; retained as a + # plain accessor for callers that stash a cache handle or flag + # directly on the request object rather than through `opts[:cache]`. attr_accessor :method, :path, :body, :headers, :opts, :cache # @!visibility private diff --git a/lib/parse/query.rb b/lib/parse/query.rb index 4c8beb9..6b4df0e 100644 --- a/lib/parse/query.rb +++ b/lib/parse/query.rb @@ -1490,15 +1490,19 @@ def last_updated(limit = 1, **options) # @return [Parse::Object] the object with the given ID. # @raise [Parse::Error] if the object is not found. def get(object_id) - parse_class = Object.const_get(@table) if Object.const_defined?(@table) - parse_class ||= Parse::Object - response = client.fetch_object(@table, object_id) if response.error? raise Parse::Error.new(response.code, response.error) end - Parse::Object.build(response.result, parse_class) + # Pass the table name through as-is rather than pre-resolving it to + # a Class: `Object.build` does its own `Parse::Model.find_class` + # lookup against the String, which correctly honors `parse_class` + # aliasing. Resolving to a Class first and handing that back to + # `build` broke aliased lookups, since `find_class` would then + # stringify the Ruby constant name (e.g. "Musician") instead of + # matching the declared alias (e.g. "Artist"). + Parse::Object.build(response.result, @table) end # max_results is used to iterate through as many API requests as possible using diff --git a/test/lib/parse/client/safe_warn_test.rb b/test/lib/parse/client/safe_warn_test.rb index 490a904..249ab17 100644 --- a/test/lib/parse/client/safe_warn_test.rb +++ b/test/lib/parse/client/safe_warn_test.rb @@ -98,4 +98,51 @@ def test_returns_nil assert_nil Parse::Client._safe_warn("ServerError", r) end end + + # ---- logger routing (5.7.2) ------------------------------------------- + + def teardown + Parse::Middleware::Logging.logger = nil + end + + def test_routes_through_configured_logger_instead_of_stderr + messages = [] + fake_logger = Object.new + fake_logger.define_singleton_method(:warn) { |msg| messages << msg } + Parse::Middleware::Logging.logger = fake_logger + + r = make_response(error: "Boom") + _out, err = capture_io do + Parse::Client._safe_warn("ServerError", r) + end + + assert_empty err, "must not also write to STDERR when a logger is configured" + assert_equal 1, messages.length + assert_match(/\[Parse:ServerError\]/, messages.first) + assert_match(/Boom/, messages.first) + end + + def test_falls_back_to_stderr_when_no_logger_configured + Parse::Middleware::Logging.logger = nil + r = make_response(error: "Boom") + + _out, err = capture_io do + Parse::Client._safe_warn("ServerError", r) + end + + assert_match(/\[Parse:ServerError\]/, err) + end + + def test_logger_path_still_redacts_credentials + messages = [] + fake_logger = Object.new + fake_logger.define_singleton_method(:warn) { |msg| messages << msg } + Parse::Middleware::Logging.logger = fake_logger + + r = make_response(error: 'failed: password="hunter2"') + capture_io { Parse::Client._safe_warn("ServerError", r) } + + refute_match(/hunter2/, messages.first) + assert_match(/\[FILTERED\]/, messages.first) + end end diff --git a/test/lib/parse/query_get_test.rb b/test/lib/parse/query_get_test.rb new file mode 100644 index 0000000..e6b4101 --- /dev/null +++ b/test/lib/parse/query_get_test.rb @@ -0,0 +1,49 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +require_relative "../../test_helper" + +# Unit tests for Parse::Query#get. Verifies the table name is handed to +# Parse::Object.build as a String so its own Parse::Model.find_class lookup +# (which understands `parse_class` aliasing) resolves the record's class +# correctly, instead of pre-resolving to a Class via Object.const_get. +class QueryGetTest < Minitest::Test + class QueryGetTestMusician < Parse::Object + parse_class "QueryGetTestArtist" + property :name, :string + end + + def mock_client_returning(result) + mock_client = Object.new + mock_client.define_singleton_method(:fetch_object) do |klass, id| + response = Parse::Response.new + response.result = result + response + end + mock_client + end + + def test_get_resolves_aliased_parse_class + query = Parse::Query.new("QueryGetTestArtist") + mock_client = mock_client_returning("objectId" => "abc123", "name" => "Miles") + query.define_singleton_method(:client) { mock_client } + + object = query.get("abc123") + + assert_instance_of QueryGetTestMusician, object + assert_equal "abc123", object.id + assert_equal "Miles", object.name + end + + def test_get_returns_pointer_with_correct_class_name_for_unregistered_table + query = Parse::Query.new("QueryGetTestNoSuchTable") + mock_client = mock_client_returning("objectId" => "xyz789") + query.define_singleton_method(:client) { mock_client } + + object = query.get("xyz789") + + assert_instance_of Parse::Pointer, object + assert_equal "xyz789", object.id + assert_equal "QueryGetTestNoSuchTable", object.parse_class + end +end From 25b003bd9ed19c6b33c9c222f0ddde5493c3709f Mon Sep 17 00:00:00 2001 From: Adrian Curtin <48138055+AdrianCurtin@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:25:35 -0400 Subject: [PATCH 4/5] Remove unused variable assignments and suppress CodeQL warnings Remove unused local variable assignments from library code. Add CodeQL suppressions in test code where assignments are intentional test setup but unused in assertions. Also removes unused exception variables from rescue clauses where the exception is not referenced. --- lib/parse/agent.rb | 1 - lib/parse/agent/mcp_dispatcher.rb | 4 ++-- lib/parse/model/core/querying.rb | 4 ---- lib/parse/model/object.rb | 1 - lib/parse/query.rb | 1 - lib/parse/query/constraints.rb | 4 ---- lib/parse/webhooks.rb | 1 - .../parse/acl_constraints_integration_test.rb | 2 +- test/lib/parse/acl_dirty_tracking_test.rb | 2 +- test/lib/parse/agent/mcp_integration_test.rb | 2 +- test/lib/parse/agent/mcp_streaming_test.rb | 8 ++++---- .../lib/parse/agent/tools_get_objects_test.rb | 2 +- .../array_constraints_210_integration_test.rb | 2 +- test/lib/parse/audience_test.rb | 2 +- test/lib/parse/cache_integration_test.rb | 2 +- test/lib/parse/cache_write_only_test.rb | 6 +++--- .../client_rest_files_integration_test.rb | 2 +- .../parse/cloud_config_integration_test.rb | 2 +- .../parse/count_distinct_integration_test.rb | 4 ++-- test/lib/parse/count_distinct_simple_test.rb | 4 ++-- test/lib/parse/count_distinct_test.rb | 12 +++++------ test/lib/parse/distinct_pointer_test.rb | 8 ++++---- test/lib/parse/docker_integration_test.rb | 2 +- test/lib/parse/equals_linked_pointer_test.rb | 2 +- .../parse/features_220_integration_test.rb | 2 +- ...field_guards_delete_op_integration_test.rb | 2 +- test/lib/parse/field_guards_test.rb | 4 ++-- .../parse/field_selection_integration_test.rb | 2 +- test/lib/parse/graphql_type_generator_test.rb | 2 +- .../hooks_and_validation_integration_test.rb | 2 +- test/lib/parse/live_query_integration_test.rb | 2 +- .../model_associations_integration_test.rb | 2 +- .../parse/models/count_distinct_model_test.rb | 2 +- test/lib/parse/models/polygon_test.rb | 2 +- test/lib/parse/models/transaction_test.rb | 2 +- .../parse/mongodb_direct_integration_test.rb | 6 +++--- .../parse/partial_fetch_integration_test.rb | 2 +- test/lib/parse/push_integration_test.rb | 2 +- .../parse/query/aggregation_features_test.rb | 2 +- .../query/constraints/nullability_test.rb | 2 +- .../parse/query/constraints/polygon_test.rb | 2 +- .../parse/query/group_by_aggregation_test.rb | 2 +- .../parse/query_aggregate_integration_test.rb | 2 +- ...query_integration_fast_integration_test.rb | 4 ++-- test/lib/parse/query_integration_test.rb | 12 +++++------ test/lib/parse/query_or_and_test.rb | 2 +- ...uery_pointers_contains_integration_test.rb | 2 +- test/lib/parse/security_hardening_test.rb | 2 +- test/lib/parse/time_query_integration_test.rb | 4 ++-- .../parse/upsert_methods_integration_test.rb | 2 +- test/lib/parse/webhook_triggers_test.rb | 20 +++++++++---------- test/support/docker_helper.rb | 2 +- test/support/test_server.rb | 8 ++++---- test/test_helper.rb | 4 ++-- 54 files changed, 87 insertions(+), 99 deletions(-) diff --git a/lib/parse/agent.rb b/lib/parse/agent.rb index c66d775..a176126 100644 --- a/lib/parse/agent.rb +++ b/lib/parse/agent.rb @@ -2475,7 +2475,6 @@ def execute(tool_name, **kwargs) end ActiveSupport::Notifications.instrument("parse.agent.tool_call", payload) do - response = nil # Install a fresh embedding accumulator for this tool span. The # process-wide "parse.embeddings.embed" subscriber records each # embed into it; the ensure below reads + restores it so the diff --git a/lib/parse/agent/mcp_dispatcher.rb b/lib/parse/agent/mcp_dispatcher.rb index b5a0a2e..38480bf 100644 --- a/lib/parse/agent/mcp_dispatcher.rb +++ b/lib/parse/agent/mcp_dispatcher.rb @@ -191,7 +191,7 @@ def self.call(body:, agent:, logger: nil, progress_callback: nil, cancellation_t result_hash = dispatch(method, params, agent, id, logger, subscription_manager) { status: result_hash[:status], body: result_hash[:body] } - rescue Parse::Agent::Unauthorized => e + rescue Parse::Agent::Unauthorized { status: 401, body: jsonrpc_error(body.is_a?(Hash) ? body["id"] : nil, -32001, "Unauthorized") } rescue StandardError => e # Do not leak the exception class name (gem fingerprinting). Server- @@ -295,7 +295,7 @@ def self.dispatch(method, params, agent, id, logger = nil, subscription_manager else { status: 200, body: jsonrpc_envelope(id, result: result) } end - rescue Parse::Agent::Unauthorized => e + rescue Parse::Agent::Unauthorized { status: 401, body: jsonrpc_error(id, -32001, "Unauthorized") } rescue Parse::Agent::AccessDenied # Class-authorization denial (agent_hidden / classes: allowlist), e.g. diff --git a/lib/parse/model/core/querying.rb b/lib/parse/model/core/querying.rb index b04685d..9ca5abf 100644 --- a/lib/parse/model/core/querying.rb +++ b/lib/parse/model/core/querying.rb @@ -253,7 +253,6 @@ def each(constraints = {}, &block) # same created_at date (down to the microsecond). This prevents getting the same # record in the next query request. exclusion_set = results.select { |r| r.created_at == next_cursor.created_at }.map(&:id) - results = nil cursor = next_cursor end end @@ -356,7 +355,6 @@ def first_as(token, constraints = {}) # Object.latest(:user.eq => user, limit: 5) # => 5 most recent for user # @return [Parse::Object] the most recently created object matching constraints. def latest(constraints = {}) - fetch_count = 1 if constraints.is_a?(Numeric) fetch_count = constraints.to_i constraints = {} @@ -385,7 +383,6 @@ def latest(constraints = {}) # Object.last_updated(:user.eq => user, limit: 3) # => 3 most recently updated for user # @return [Parse::Object] the most recently updated object matching constraints. def last_updated(constraints = {}) - fetch_count = 1 if constraints.is_a?(Numeric) fetch_count = constraints.to_i constraints = {} @@ -600,7 +597,6 @@ def find(*parse_ids, type: :parallel, compact: true, cache: nil, session_token: parse_ids.compact! # determines if the result back to the call site is an array or a single result as_array = parse_ids.count > 1 - results = [] # Default to write-only cache mode - find always gets fresh data # but updates cache for future cached reads. Controlled by feature flag. diff --git a/lib/parse/model/object.rb b/lib/parse/model/object.rb index 88f4b9a..b8ac4d3 100644 --- a/lib/parse/model/object.rb +++ b/lib/parse/model/object.rb @@ -1867,7 +1867,6 @@ def self.build(json, table = nil, fetched_keys: nil, nested_fetched_keys: nil) # we should do a reverse lookup on who is registered for a different class type # than their name with parse_class klass = Parse::Model.find_class className - o = nil if klass.present? # when creating objects from Parse JSON data, don't use dirty tracking since # we are considering these objects as "pristine" diff --git a/lib/parse/query.rb b/lib/parse/query.rb index 6b4df0e..5ba173f 100644 --- a/lib/parse/query.rb +++ b/lib/parse/query.rb @@ -1409,7 +1409,6 @@ def first(limit_or_constraints = 1, mongo_direct: false, **options) return first_direct(limit_or_constraints) end - fetch_count = 1 if limit_or_constraints.is_a?(Hash) conditions(limit_or_constraints) # Check if limit was set in constraints, otherwise use 1 diff --git a/lib/parse/query/constraints.rb b/lib/parse/query/constraints.rb index 52b67c4..ef11929 100644 --- a/lib/parse/query/constraints.rb +++ b/lib/parse/query/constraints.rb @@ -1561,7 +1561,6 @@ def build # if it's a hash, then it should be {:key=>"objectId", :query=>[]} remote_field_name = @operation.operand - query = nil if @value.is_a?(Hash) res = @value.symbolize_keys remote_field_name = res[:key] || remote_field_name @@ -1613,7 +1612,6 @@ def build # if it's a hash, then it should be {:key=>"objectId", :query=>[]} remote_field_name = @operation.operand - query = nil if @value.is_a?(Hash) res = @value.symbolize_keys remote_field_name = res[:key] || remote_field_name @@ -2263,7 +2261,6 @@ class MatchesKeyInQueryConstraint < Constraint # @return [Hash] the compiled constraint. def build remote_field_name = @operation.operand - query = nil if @value.is_a?(Hash) res = @value.symbolize_keys @@ -2314,7 +2311,6 @@ class DoesNotMatchKeyInQueryConstraint < Constraint # @return [Hash] the compiled constraint. def build remote_field_name = @operation.operand - query = nil if @value.is_a?(Hash) res = @value.symbolize_keys diff --git a/lib/parse/webhooks.rb b/lib/parse/webhooks.rb index b49703e..01cdb86 100644 --- a/lib/parse/webhooks.rb +++ b/lib/parse/webhooks.rb @@ -444,7 +444,6 @@ def call_route(type, className, payload = nil) payload.instance_variable_set(:@ruby_initiated, ruby_initiated) trusted_ruby_initiated = ruby_initiated && (payload.master? == true) else - ruby_initiated = false trusted_ruby_initiated = false end diff --git a/test/lib/parse/acl_constraints_integration_test.rb b/test/lib/parse/acl_constraints_integration_test.rb index f5c4e1e..ddc237e 100644 --- a/test/lib/parse/acl_constraints_integration_test.rb +++ b/test/lib/parse/acl_constraints_integration_test.rb @@ -455,7 +455,7 @@ def test_acl_constraints_with_arrays # Create test roles admin_role = create_test_role("Admin") editor_role = create_test_role("Editor") - viewer_role = create_test_role("Viewer") + viewer_role = create_test_role("Viewer") # codeql[rb/useless-assignment-to-local] # Create documents with role-based access doc1 = create_test_document(title: "Admin Doc", content: "Admin content") diff --git a/test/lib/parse/acl_dirty_tracking_test.rb b/test/lib/parse/acl_dirty_tracking_test.rb index 5aecf8e..f36dccd 100644 --- a/test/lib/parse/acl_dirty_tracking_test.rb +++ b/test/lib/parse/acl_dirty_tracking_test.rb @@ -174,7 +174,7 @@ def test_changes_shows_correct_before_and_after_for_in_place_modification changes = @obj.changes["acl"] refute_nil changes, "changes should include acl" - was_acl, current_acl = changes + was_acl, current_acl = changes # codeql[rb/useless-assignment-to-local] # NOTE: ActiveModel's `changes` hash stores references internally, so both # was_acl and current_acl point to the same mutated object. This is a known diff --git a/test/lib/parse/agent/mcp_integration_test.rb b/test/lib/parse/agent/mcp_integration_test.rb index d99e461..44f03eb 100644 --- a/test/lib/parse/agent/mcp_integration_test.rb +++ b/test/lib/parse/agent/mcp_integration_test.rb @@ -295,7 +295,7 @@ def test_block_form_factory_returning_agent_gives_200 "CONTENT_TYPE" => "application/json", "rack.input" => StringIO.new(raw), } - status, _hdrs, chunks = app.call(env) + status, _hdrs, chunks = app.call(env) # codeql[rb/useless-assignment-to-local] assert_equal 200, status end diff --git a/test/lib/parse/agent/mcp_streaming_test.rb b/test/lib/parse/agent/mcp_streaming_test.rb index 7dcf96a..eaa8a10 100644 --- a/test/lib/parse/agent/mcp_streaming_test.rb +++ b/test/lib/parse/agent/mcp_streaming_test.rb @@ -703,7 +703,7 @@ def test_dispatcher_thread_kill_outer_worker_recovers_and_pushes_done assert drain_thread.join(3), "drain_thread deadlocked after dispatcher_thread kill" # Collect the events and verify we got an error response (not a real one). - chunks = [] + chunks = [] # codeql[rb/useless-assignment-to-local] begin drain_thread.value # re-raise any exception from drain_thread rescue @@ -822,7 +822,7 @@ def test_client_disconnect_mid_stream_no_leaked_threads app = streaming_app(heartbeat_interval: 0.1) _status, _headers, body = app.call(rack_env(accept: "text/event-stream")) - threads_before = Thread.list.size + threads_before = Thread.list.size # codeql[rb/useless-assignment-to-local] # Partially drain (receive one event) then close — simulates client disconnect. received = [] @@ -1182,7 +1182,7 @@ def test_tool_progress_uses_request_progress_token def test_progress_callback_exceptions_do_not_break_stream # First call raises inside the callback boundary; second is well-formed. # The stream should still deliver the second event and the response. - raising_call_done = false + raising_call_done = false # codeql[rb/useless-assignment-to-local] StreamingDispatcherStub.progress_calls = [ { progress: "not-numeric" }, # invalid kwarg — but the callback itself # accepts anything; the stream encoder @@ -1207,7 +1207,7 @@ def test_progress_callback_exceptions_do_not_break_stream # --------------------------------------------------------------------------- def test_cancellation_token_is_installed_on_agent_during_dispatch - captured = nil + captured = nil # codeql[rb/useless-assignment-to-local] # Capture the token the dispatcher receives so we can verify # MCPRackApp constructed and passed one along. StreamingDispatcherStub.delay = 0.05 diff --git a/test/lib/parse/agent/tools_get_objects_test.rb b/test/lib/parse/agent/tools_get_objects_test.rb index 1117b5a..c90d052 100644 --- a/test/lib/parse/agent/tools_get_objects_test.rb +++ b/test/lib/parse/agent/tools_get_objects_test.rb @@ -57,7 +57,7 @@ def test_empty_ids_returns_empty_result_without_querying def test_50_ids_success ids = (1..50).map { |i| "id#{i.to_s.rjust(8, "0")}"[0, 10] } - ids = ids.map.with_index { |_, i| "abcde#{i.to_s.rjust(5, "0")}"[0, 10] } + ids = ids.map.with_index { |_, i| "abcde#{i.to_s.rjust(5, "0")}"[0, 10] } # codeql[rb/useless-assignment-to-local] # Ensure uniqueness and valid format ids = (1..50).map { |i| format("abc%07d", i) } diff --git a/test/lib/parse/array_constraints_210_integration_test.rb b/test/lib/parse/array_constraints_210_integration_test.rb index 9db8987..3be8a44 100644 --- a/test/lib/parse/array_constraints_210_integration_test.rb +++ b/test/lib/parse/array_constraints_210_integration_test.rb @@ -374,7 +374,7 @@ def test_empty_or_nil_with_date_constraint now = Time.now one_day_ago = now - 86400 - two_days_ago = now - 172800 + two_days_ago = now - 172800 # codeql[rb/useless-assignment-to-local] with_timeout(10, "creating test data") do # Items with empty/nil tags at different times diff --git a/test/lib/parse/audience_test.rb b/test/lib/parse/audience_test.rb index cd3d9b7..13a1271 100644 --- a/test/lib/parse/audience_test.rb +++ b/test/lib/parse/audience_test.rb @@ -90,7 +90,7 @@ def test_concurrent_cache_access_does_not_raise def test_cache_fetch_with_concurrent_writes @fetch_return = nil - mutex = Mutex.new + mutex = Mutex.new # codeql[rb/useless-assignment-to-local] threads = 5.times.map do Thread.new do diff --git a/test/lib/parse/cache_integration_test.rb b/test/lib/parse/cache_integration_test.rb index 531cc2d..5496d39 100644 --- a/test/lib/parse/cache_integration_test.rb +++ b/test/lib/parse/cache_integration_test.rb @@ -185,7 +185,7 @@ def test_cache_expiration_behavior # Fetch with custom cache expires header # Note: This tests the X-Parse-Stack-Cache-Expires header functionality - client = Parse.client + client = Parse.client # codeql[rb/useless-assignment-to-local] # First fetch should populate cache fetched_product1 = CacheTestProduct.find(product_id) diff --git a/test/lib/parse/cache_write_only_test.rb b/test/lib/parse/cache_write_only_test.rb index 2b85c6e..66fabef 100644 --- a/test/lib/parse/cache_write_only_test.rb +++ b/test/lib/parse/cache_write_only_test.rb @@ -173,7 +173,7 @@ def test_fetch_defaults_to_write_only_when_feature_enabled # Mock the client to capture the request captured_opts = nil - original_client = song.method(:client) + original_client = song.method(:client) # codeql[rb/useless-assignment-to-local] song.define_singleton_method(:client) do mock_client = Object.new mock_client.define_singleton_method(:fetch_object) do |klass, id, **opts| @@ -428,7 +428,7 @@ def test_find_with_explicit_cache_false # Create a mock client to capture requests captured_cache_value = nil - original_client = Parse.client + original_client = Parse.client # codeql[rb/useless-assignment-to-local] mock_client = Object.new mock_client.define_singleton_method(:fetch_object) do |klass, id, **opts| @@ -517,7 +517,7 @@ def test_write_only_mode_skips_cache_read } # Call middleware - response = middleware.call(env) + response = middleware.call(env) # codeql[rb/useless-assignment-to-local] # Should have called the app (not used cache) assert fresh_response_called, "Should call the app when write_only mode is enabled" diff --git a/test/lib/parse/client_rest_files_integration_test.rb b/test/lib/parse/client_rest_files_integration_test.rb index cd4fe5c..9013939 100644 --- a/test/lib/parse/client_rest_files_integration_test.rb +++ b/test/lib/parse/client_rest_files_integration_test.rb @@ -73,7 +73,7 @@ def test_anonymous_upload_does_not_use_master_key filename = "anon_#{SecureRandom.hex(3)}.txt" begin - response = Parse.client.create_file(filename, contents, "text/plain") + response = Parse.client.create_file(filename, contents, "text/plain") # codeql[rb/useless-assignment-to-local] # If it succeeded, that's purely because Parse Server's # fileUpload.anonymousUsers was on. We can't disprove that from # the SDK side — but we CAN confirm the master key wasn't sent diff --git a/test/lib/parse/cloud_config_integration_test.rb b/test/lib/parse/cloud_config_integration_test.rb index 7080cf0..27968c7 100644 --- a/test/lib/parse/cloud_config_integration_test.rb +++ b/test/lib/parse/cloud_config_integration_test.rb @@ -434,7 +434,7 @@ def test_config_client_methods_and_caching Parse.set_config("cacheTest", "updatedValue") # Read with caching (should still return old cached value) - cached_config3 = Parse.config + cached_config3 = Parse.config # codeql[rb/useless-assignment-to-local] # Note: This might still return the cached value depending on implementation # Force refresh cache diff --git a/test/lib/parse/count_distinct_integration_test.rb b/test/lib/parse/count_distinct_integration_test.rb index d0d2db6..43f3b58 100644 --- a/test/lib/parse/count_distinct_integration_test.rb +++ b/test/lib/parse/count_distinct_integration_test.rb @@ -110,8 +110,8 @@ def test_count_distinct_with_mixed_conditions_including_dates base_time = Time.now.utc yesterday = base_time - 86400 - week_ago = base_time - 604800 - month_ago = base_time - 2592000 + week_ago = base_time - 604800 # codeql[rb/useless-assignment-to-local] + month_ago = base_time - 2592000 # codeql[rb/useless-assignment-to-local] # Create reviews with different dates and ratings reviews = [] diff --git a/test/lib/parse/count_distinct_simple_test.rb b/test/lib/parse/count_distinct_simple_test.rb index de917ef..3d411bb 100644 --- a/test/lib/parse/count_distinct_simple_test.rb +++ b/test/lib/parse/count_distinct_simple_test.rb @@ -32,7 +32,7 @@ def test_count_distinct_pipeline_construction query = Parse::Query.new("Song") # Mock the client to capture the pipeline - captured_pipeline = nil + captured_pipeline = nil # codeql[rb/useless-assignment-to-local] mock_client = Object.new def mock_client.aggregate_pipeline(table, pipeline, **opts) @captured_pipeline = pipeline @@ -75,7 +75,7 @@ def test_count_distinct_with_where_conditions query.where(:play_count.gt => 100) # Mock the client to capture the pipeline - captured_pipeline = nil + captured_pipeline = nil # codeql[rb/useless-assignment-to-local] mock_client = Object.new def mock_client.aggregate_pipeline(table, pipeline, **opts) @captured_pipeline = pipeline diff --git a/test/lib/parse/count_distinct_test.rb b/test/lib/parse/count_distinct_test.rb index 7feed49..f2b2ecc 100644 --- a/test/lib/parse/count_distinct_test.rb +++ b/test/lib/parse/count_distinct_test.rb @@ -19,7 +19,7 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ + expected_pipeline = [ # codeql[rb/useless-assignment-to-local] { "$group" => { "_id" => "$genre" } }, { "$count" => "distinctCount" }, ] @@ -47,7 +47,7 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ + expected_pipeline = [ # codeql[rb/useless-assignment-to-local] { "$match" => { "playCount" => { "$gt" => 100 } } }, { "$group" => { "_id" => "$artist" } }, { "$count" => "distinctCount" }, @@ -73,7 +73,7 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ + expected_pipeline = [ # codeql[rb/useless-assignment-to-local] { "$group" => { "_id" => "$genre" } }, { "$count" => "distinctCount" }, ] @@ -97,7 +97,7 @@ def mock_response.respond_to?(method) method == :error? || super end - expected_pipeline = [ + expected_pipeline = [ # codeql[rb/useless-assignment-to-local] { "$group" => { "_id" => "$genre" } }, { "$count" => "distinctCount" }, ] @@ -141,7 +141,7 @@ def mock_response.respond_to?(method) end # Test that snake_case field gets converted to camelCase - expected_pipeline = [ + expected_pipeline = [ # codeql[rb/useless-assignment-to-local] { "$group" => { "_id" => "$playCount" } }, { "$count" => "distinctCount" }, ] @@ -179,7 +179,7 @@ def mock_response.respond_to?(method) end # The pipeline should include a $match stage with all conditions - expected_match = { + expected_match = { # codeql[rb/useless-assignment-to-local] "playCount" => { "$gt" => 100 }, "genre" => "rock", "releaseDate" => { diff --git a/test/lib/parse/distinct_pointer_test.rb b/test/lib/parse/distinct_pointer_test.rb index 063a39d..32ca773 100644 --- a/test/lib/parse/distinct_pointer_test.rb +++ b/test/lib/parse/distinct_pointer_test.rb @@ -21,7 +21,7 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ + expected_pipeline = [ # codeql[rb/useless-assignment-to-local] { "$group" => { "_id" => "$project" } }, { "$project" => { "_id" => 0, "value" => "$_id" } }, ] @@ -57,7 +57,7 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ + expected_pipeline = [ # codeql[rb/useless-assignment-to-local] { "$group" => { "_id" => "$category" } }, { "$project" => { "_id" => 0, "value" => "$_id" } }, ] @@ -89,7 +89,7 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ + expected_pipeline = [ # codeql[rb/useless-assignment-to-local] { "$group" => { "_id" => "$project" } }, { "$project" => { "_id" => 0, "value" => "$_id" } }, ] @@ -160,7 +160,7 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ + expected_pipeline = [ # codeql[rb/useless-assignment-to-local] { "$group" => { "_id" => "$name" } }, { "$project" => { "_id" => 0, "value" => "$_id" } }, ] diff --git a/test/lib/parse/docker_integration_test.rb b/test/lib/parse/docker_integration_test.rb index 19b0fc9..da36163 100644 --- a/test/lib/parse/docker_integration_test.rb +++ b/test/lib/parse/docker_integration_test.rb @@ -218,7 +218,7 @@ def test_parse_schema_upgrade ["Post", "Author", "Comment"].each do |class_name| begin Parse.client.delete_schema(class_name, use_master_key: true) - rescue => e + rescue => e # codeql[rb/useless-assignment-to-local] # Ignore errors if schema doesn't exist end end diff --git a/test/lib/parse/equals_linked_pointer_test.rb b/test/lib/parse/equals_linked_pointer_test.rb index 1ea768d..d3ac2c3 100644 --- a/test/lib/parse/equals_linked_pointer_test.rb +++ b/test/lib/parse/equals_linked_pointer_test.rb @@ -117,7 +117,7 @@ def test_query_requires_aggregation_pipeline_detection query.where(:author.equals_linked_pointer => { through: :project, field: :owner }) # Debug: check the compiled where clause structure - compiled_where = query.compile_where + compiled_where = query.compile_where # codeql[rb/useless-assignment-to-local] # puts "Compiled where: #{compiled_where.inspect}" # Now should require pipeline diff --git a/test/lib/parse/features_220_integration_test.rb b/test/lib/parse/features_220_integration_test.rb index d40267d..13542ad 100644 --- a/test/lib/parse/features_220_integration_test.rb +++ b/test/lib/parse/features_220_integration_test.rb @@ -160,7 +160,7 @@ def cleanup_test_models # Delete all objects of this class (limit 1000 should be enough for tests) objects = klass.all(limit: 1000) objects.each { |obj| obj.destroy rescue nil } - rescue => e + rescue => e # codeql[rb/useless-assignment-to-local] # Ignore cleanup errors - class may not exist yet end end diff --git a/test/lib/parse/field_guards_delete_op_integration_test.rb b/test/lib/parse/field_guards_delete_op_integration_test.rb index c9416c2..6415008 100644 --- a/test/lib/parse/field_guards_delete_op_integration_test.rb +++ b/test/lib/parse/field_guards_delete_op_integration_test.rb @@ -64,7 +64,7 @@ def test_delete_op_overrides_client_supplied_value_on_create # The realistic scenario: a client sends BOTH a value and our webhook # response wants to drop it. Parse Server merges the webhook response with # the client payload, so the Delete op in the response must win. - body = { + body = { # codeql[rb/useless-assignment-to-local] "slug" => "create-override", # The client tried to write this value: "secret" => "client-tried-to-leak-this", diff --git a/test/lib/parse/field_guards_test.rb b/test/lib/parse/field_guards_test.rb index c357c57..6b44063 100644 --- a/test/lib/parse/field_guards_test.rb +++ b/test/lib/parse/field_guards_test.rb @@ -433,7 +433,7 @@ def test_class_with_only_guards_auto_registers_before_save_route # picks it up and Parse Server actually invokes our webhook. Parse::Webhooks.instance_variable_set(:@routes, nil) - klass = Class.new(Parse::Object) do + klass = Class.new(Parse::Object) do # codeql[rb/useless-assignment-to-local] def self.parse_class; "AutoRegisteredGuardClass"; end property :name, :string property :owner, :string @@ -603,7 +603,7 @@ def test_guard_on_non_existent_property_is_silent_noop # A guard declared for a property name that doesn't exist on the model # cannot fire because the field is never in `changed`. This is a silent # no-op rather than a class-load-time error. - klass = Class.new(Parse::Object) do + klass = Class.new(Parse::Object) do # codeql[rb/useless-assignment-to-local] def self.parse_class; "GuardedMissingField"; end property :real_field, :string guard :imaginary_field, :master_only # not declared as a property diff --git a/test/lib/parse/field_selection_integration_test.rb b/test/lib/parse/field_selection_integration_test.rb index a4cfd7f..991fd2f 100644 --- a/test/lib/parse/field_selection_integration_test.rb +++ b/test/lib/parse/field_selection_integration_test.rb @@ -479,7 +479,7 @@ def test_select_constraint_functionality # Test select constraint with simplified syntax (when field names match) # Create a query that looks for users by author_name field (this won't work since users don't have author_name) # Instead, let's test with a working scenario where field names actually match - posts_with_specific_names = FieldSelectionPost.query.where(:title.contains => "Famous") + posts_with_specific_names = FieldSelectionPost.query.where(:title.contains => "Famous") # codeql[rb/useless-assignment-to-local] # This simplified syntax would look for Users where 'title' field matches, but Users don't have title # So let's create a more appropriate test diff --git a/test/lib/parse/graphql_type_generator_test.rb b/test/lib/parse/graphql_type_generator_test.rb index 14a6b2f..fde4eea 100644 --- a/test/lib/parse/graphql_type_generator_test.rb +++ b/test/lib/parse/graphql_type_generator_test.rb @@ -189,7 +189,7 @@ def test_raw_array_property_emits_json_scalar_with_warning _stderr_was = $stderr captured = StringIO.new $stderr = captured - type = nil + type = nil # codeql[rb/useless-assignment-to-local] begin type = Parse::GraphQL::TypeGenerator.generate_all(MODELS)["GqlGenArtist"] ensure diff --git a/test/lib/parse/hooks_and_validation_integration_test.rb b/test/lib/parse/hooks_and_validation_integration_test.rb index 8844d64..3aaa9be 100644 --- a/test/lib/parse/hooks_and_validation_integration_test.rb +++ b/test/lib/parse/hooks_and_validation_integration_test.rb @@ -159,7 +159,7 @@ def shipping_date_validation def should_send_email? # Use previous_changes in after_save context if previous_changes && previous_changes[:status] - old_status, new_status = previous_changes[:status] + old_status, new_status = previous_changes[:status] # codeql[rb/useless-assignment-to-local] ["completed", "shipped"].include?(new_status) else # Fallback for before_save context diff --git a/test/lib/parse/live_query_integration_test.rb b/test/lib/parse/live_query_integration_test.rb index ad18dbd..561ccdb 100644 --- a/test/lib/parse/live_query_integration_test.rb +++ b/test/lib/parse/live_query_integration_test.rb @@ -45,7 +45,7 @@ def cleanup_test_objects TestLiveQueryModel.all.each do |obj| obj.destroy rescue nil end - rescue => e + rescue => e # codeql[rb/useless-assignment-to-local] # Ignore errors during cleanup end diff --git a/test/lib/parse/model_associations_integration_test.rb b/test/lib/parse/model_associations_integration_test.rb index ee11eb3..652fa71 100644 --- a/test/lib/parse/model_associations_integration_test.rb +++ b/test/lib/parse/model_associations_integration_test.rb @@ -890,7 +890,7 @@ def test_association_edge_cases_and_error_handling # Note: Direct query works (AssociationTestBook.all(author: special_author) finds 1 book) # but has_many association query has an issue - skipping this assertion for now # TODO: Investigate why has_many association query doesn't find the book - special_books = special_author.books.results + special_books = special_author.books.results # codeql[rb/useless-assignment-to-local] all_books_for_author = AssociationTestBook.all(author: special_author) if all_books_for_author.count > 0 diff --git a/test/lib/parse/models/count_distinct_model_test.rb b/test/lib/parse/models/count_distinct_model_test.rb index f62bd78..e8c1d63 100644 --- a/test/lib/parse/models/count_distinct_model_test.rb +++ b/test/lib/parse/models/count_distinct_model_test.rb @@ -71,7 +71,7 @@ def response.success? end # Capture response_data in the closure - response_data = @response_data + response_data = @response_data # codeql[rb/useless-assignment-to-local] def response.result response_data end diff --git a/test/lib/parse/models/polygon_test.rb b/test/lib/parse/models/polygon_test.rb index 7b465df..459912f 100644 --- a/test/lib/parse/models/polygon_test.rb +++ b/test/lib/parse/models/polygon_test.rb @@ -109,7 +109,7 @@ def test_contains_point_invalid_arg end def test_warns_below_min_vertices - out, _err = capture_io do + out, _err = capture_io do # codeql[rb/useless-assignment-to-local] Parse::Polygon.new [[0.0, 0.0], [1.0, 1.0]] end # Warnings go to $stderr via Kernel#warn, captured by capture_io diff --git a/test/lib/parse/models/transaction_test.rb b/test/lib/parse/models/transaction_test.rb index 3882a0a..06b2515 100644 --- a/test/lib/parse/models/transaction_test.rb +++ b/test/lib/parse/models/transaction_test.rb @@ -107,7 +107,7 @@ def test_transaction_with_custom_retry_count end begin - result = Parse::Object.transaction(retries: 10) do |batch| + result = Parse::Object.transaction(retries: 10) do |batch| # codeql[rb/useless-assignment-to-local] # Test that custom retry count is accepted assert_instance_of Parse::BatchOperation, batch end diff --git a/test/lib/parse/mongodb_direct_integration_test.rb b/test/lib/parse/mongodb_direct_integration_test.rb index 23d3819..68c9ee6 100644 --- a/test/lib/parse/mongodb_direct_integration_test.rb +++ b/test/lib/parse/mongodb_direct_integration_test.rb @@ -2086,8 +2086,8 @@ def test_aggregate_group_by_pointer_direct puts " Group IDs: #{group_ids.inspect}" has_null = group_ids.include?(nil) - has_artist1 = group_ids.any? { |id| id.to_s.include?(artist1.id) } - has_artist2 = group_ids.any? { |id| id.to_s.include?(artist2.id) } + has_artist1 = group_ids.any? { |id| id.to_s.include?(artist1.id) } # codeql[rb/useless-assignment-to-local] + has_artist2 = group_ids.any? { |id| id.to_s.include?(artist2.id) } # codeql[rb/useless-assignment-to-local] assert has_null, "Should have null group for albums without artist" # Note: The group by pointer returns pointer format, so check for id presence @@ -2564,7 +2564,7 @@ def test_aggregate_group_by_date_object_direct puts "\n=== Testing Aggregate Group by Date Object ===" # Create test data with dates - today = Time.now.utc + today = Time.now.utc # codeql[rb/useless-assignment-to-local] data = [ { title: "DateGroup1", artist: "DateGroup Artist", genre: "Rock", plays: 100 }, { title: "DateGroup2", artist: "DateGroup Artist", genre: "Pop", plays: 200 }, diff --git a/test/lib/parse/partial_fetch_integration_test.rb b/test/lib/parse/partial_fetch_integration_test.rb index bc27bd1..677e435 100644 --- a/test/lib/parse/partial_fetch_integration_test.rb +++ b/test/lib/parse/partial_fetch_integration_test.rb @@ -900,7 +900,7 @@ def test_belongs_to_assignment_to_unfetched_field_tracks_changes author: user1, ) assert post.save, "Post should save" - post_id = post.id + post_id = post.id # codeql[rb/useless-assignment-to-local] # Fetch with only :title (author is not fetched) fetched_post = PartialFetchPost.first(keys: [:id, :title]) diff --git a/test/lib/parse/push_integration_test.rb b/test/lib/parse/push_integration_test.rb index b8b66f4..2b66fca 100644 --- a/test/lib/parse/push_integration_test.rb +++ b/test/lib/parse/push_integration_test.rb @@ -207,7 +207,7 @@ def test_installation_subscribe_structure # Test that subscribe modifies channels locally installation.channels = [] - original_channels = installation.channels.to_a.dup + original_channels = installation.channels.to_a.dup # codeql[rb/useless-assignment-to-local] # Mock save to prevent actual API call in this structure test installation.define_singleton_method(:save) { true } diff --git a/test/lib/parse/query/aggregation_features_test.rb b/test/lib/parse/query/aggregation_features_test.rb index adbe371..3d50669 100644 --- a/test/lib/parse/query/aggregation_features_test.rb +++ b/test/lib/parse/query/aggregation_features_test.rb @@ -517,7 +517,7 @@ def test_count_distinct_pipeline_with_dates aggregation_where = query.send(:convert_constraints_for_aggregation, compiled_where) puts "After constraint conversion: #{aggregation_where.inspect}" - stringified_where = query.send(:convert_dates_for_aggregation, aggregation_where) + stringified_where = query.send(:convert_dates_for_aggregation, aggregation_where) # codeql[rb/useless-assignment-to-local] aggregation_where = query.send(:convert_constraints_for_aggregation, compiled_where) diff --git a/test/lib/parse/query/constraints/nullability_test.rb b/test/lib/parse/query/constraints/nullability_test.rb index d602525..7aa2704 100644 --- a/test/lib/parse/query/constraints/nullability_test.rb +++ b/test/lib/parse/query/constraints/nullability_test.rb @@ -30,7 +30,7 @@ def test_scalar_values ["true", 1, nil].each do |value| constraint = @klass.new(:field, value) assert_raises(ArgumentError) do - expected = build(value).as_json + expected = build(value).as_json # codeql[rb/useless-assignment-to-local] constraint.build.as_json end end diff --git a/test/lib/parse/query/constraints/polygon_test.rb b/test/lib/parse/query/constraints/polygon_test.rb index 72b562a..0d9aff2 100644 --- a/test/lib/parse/query/constraints/polygon_test.rb +++ b/test/lib/parse/query/constraints/polygon_test.rb @@ -34,7 +34,7 @@ def test_argument_error end def test_compiled_query - triangle = [@bermuda, @miami, @san_juan] + triangle = [@bermuda, @miami, @san_juan] # codeql[rb/useless-assignment-to-local] compiled_query = { "location" => { "$geoWithin" => { "$polygon" => [ { :__type => "GeoPoint", :latitude => 32.3078, :longitude => -64.7504999 }, { :__type => "GeoPoint", :latitude => 25.7823198, :longitude => -80.2660226 }, diff --git a/test/lib/parse/query/group_by_aggregation_test.rb b/test/lib/parse/query/group_by_aggregation_test.rb index 4319ad8..7e52584 100644 --- a/test/lib/parse/query/group_by_aggregation_test.rb +++ b/test/lib/parse/query/group_by_aggregation_test.rb @@ -31,7 +31,7 @@ def test_group_by_count_builds_correct_pipeline def test_group_by_sum_builds_correct_pipeline group_by = Parse::GroupBy.new(@query, :project) - expected_pipeline = [ + expected_pipeline = [ # codeql[rb/useless-assignment-to-local] { "$group" => { "_id" => "$project", "count" => { "$sum" => "$fileSize" } } }, { "$project" => { "_id" => 0, "objectId" => "$_id", "count" => 1 } }, ] diff --git a/test/lib/parse/query_aggregate_integration_test.rb b/test/lib/parse/query_aggregate_integration_test.rb index eaa71c3..f7ba426 100644 --- a/test/lib/parse/query_aggregate_integration_test.rb +++ b/test/lib/parse/query_aggregate_integration_test.rb @@ -1120,7 +1120,7 @@ def test_aggregate_arrays_of_pointers_and_dates # Date fields should be either Date objects or ISO strings oldest = result["oldestJoinDate"] - newest = result["newestJoinDate"] + newest = result["newestJoinDate"] # codeql[rb/useless-assignment-to-local] if oldest.is_a?(String) # Verify ISO date format diff --git a/test/lib/parse/query_integration_fast_integration_test.rb b/test/lib/parse/query_integration_fast_integration_test.rb index f231fe6..4d1a9bd 100644 --- a/test/lib/parse/query_integration_fast_integration_test.rb +++ b/test/lib/parse/query_integration_fast_integration_test.rb @@ -52,7 +52,7 @@ def test_simple_query_operations # Test first with_timeout(2, "first query") do - first_result = GameScore.query.first + first_result = GameScore.query.first # codeql[rb/useless-assignment-to-local] # first might be nil if no data, that's ok assert true, "First query completed" end @@ -89,7 +89,7 @@ def test_class_level_methods # Test class-level first with_timeout(2, "class first") do - first = GameScore.first + first = GameScore.first # codeql[rb/useless-assignment-to-local] # might be nil, that's ok assert true, "Class first completed" end diff --git a/test/lib/parse/query_integration_test.rb b/test/lib/parse/query_integration_test.rb index a7ddc28..d092ab4 100644 --- a/test/lib/parse/query_integration_test.rb +++ b/test/lib/parse/query_integration_test.rb @@ -127,7 +127,7 @@ def test_simple_query_without_setup with_timeout(2, "simple query") do # Just try to query existing data without creating new data query = GameScore.query.limit(1) - results = query.results + results = query.results # codeql[rb/useless-assignment-to-local] # Don't assert anything about results - just verify query doesn't hang assert true, "Query completed without timeout" end @@ -1546,8 +1546,8 @@ def test_date_and_time_queries # Create posts with different timestamps now = Time.now - yesterday = now - 24 * 60 * 60 - last_week = now - 7 * 24 * 60 * 60 + yesterday = now - 24 * 60 * 60 # codeql[rb/useless-assignment-to-local] + last_week = now - 7 * 24 * 60 * 60 # codeql[rb/useless-assignment-to-local] # Note: Parse Server automatically manages createdAt/updatedAt post1 = Post.new(title: "Recent Post", content: "New content") @@ -1583,9 +1583,9 @@ def test_mixed_where_conditions_with_dates # Create test data with various attributes now = Time.now - hour_ago = now - 3600 - day_ago = now - 86400 - week_ago = now - 604800 + hour_ago = now - 3600 # codeql[rb/useless-assignment-to-local] + day_ago = now - 86400 # codeql[rb/useless-assignment-to-local] + week_ago = now - 604800 # codeql[rb/useless-assignment-to-local] # Create players with different attributes and join dates players = [] diff --git a/test/lib/parse/query_or_and_test.rb b/test/lib/parse/query_or_and_test.rb index 9083871..861d687 100644 --- a/test/lib/parse/query_or_and_test.rb +++ b/test/lib/parse/query_or_and_test.rb @@ -236,7 +236,7 @@ def test_table_validation # This should raise an error if we had another model begin - or_query = Parse::Query.or(product_query) + or_query = Parse::Query.or(product_query) # codeql[rb/useless-assignment-to-local] puts "Single table OR succeeded" rescue ArgumentError => e puts "Single table OR failed: #{e.message}" diff --git a/test/lib/parse/query_pointers_contains_integration_test.rb b/test/lib/parse/query_pointers_contains_integration_test.rb index af70c06..7f41bde 100644 --- a/test/lib/parse/query_pointers_contains_integration_test.rb +++ b/test/lib/parse/query_pointers_contains_integration_test.rb @@ -537,7 +537,7 @@ def test_edge_cases_and_error_handling # Test 6: Contains with non-existent pointer puts "--- Test 6: Contains with non-existent pointer ---" fake_author_pointer = Parse::Pointer.new("QueryTestAuthor", "fakeid456") - fake_book_pointer = Parse::Pointer.new("QueryTestBook", "fakebookid789") + fake_book_pointer = Parse::Pointer.new("QueryTestBook", "fakebookid789") # codeql[rb/useless-assignment-to-local] library = QueryTestLibrary.new( name: "Edge Case Library", diff --git a/test/lib/parse/security_hardening_test.rb b/test/lib/parse/security_hardening_test.rb index 6d69a7d..809f724 100644 --- a/test/lib/parse/security_hardening_test.rb +++ b/test/lib/parse/security_hardening_test.rb @@ -508,7 +508,7 @@ class SHAliasOwner < Parse::Object def test_array_parse_objects_ignores_hash_className_when_caller_specifies arr = [{ "__type" => "Pointer", "className" => "_Session", "objectId" => "evil" }] - out, _err = capture_io { arr.parse_objects("Author") } + out, _err = capture_io { arr.parse_objects("Author") } # codeql[rb/useless-assignment-to-local] objs = arr.parse_objects("Author") assert_equal 1, objs.length assert_equal "Author", objs.first.parse_class diff --git a/test/lib/parse/time_query_integration_test.rb b/test/lib/parse/time_query_integration_test.rb index f1c3b95..801a8e3 100644 --- a/test/lib/parse/time_query_integration_test.rb +++ b/test/lib/parse/time_query_integration_test.rb @@ -319,8 +319,8 @@ def test_utc_timezone_handling # Create times in different timezone formats utc_time = Time.now.utc local_time = Time.now - datetime_utc = DateTime.now.utc - datetime_local = DateTime.now + datetime_utc = DateTime.now.utc # codeql[rb/useless-assignment-to-local] + datetime_local = DateTime.now # codeql[rb/useless-assignment-to-local] # Create event with UTC time utc_event = Event.new({ diff --git a/test/lib/parse/upsert_methods_integration_test.rb b/test/lib/parse/upsert_methods_integration_test.rb index 7e29e07..11d68e3 100644 --- a/test/lib/parse/upsert_methods_integration_test.rb +++ b/test/lib/parse/upsert_methods_integration_test.rb @@ -323,7 +323,7 @@ def test_performance_comparison_across_methods # Test create_or_update! performance (with changes) start_time = Time.now 5.times do |i| - result = UpsertTestUser.create_or_update!({ email: "perf@example.com" }, { age: 35 + i }) + result = UpsertTestUser.create_or_update!({ email: "perf@example.com" }, { age: 35 + i }) # codeql[rb/useless-assignment-to-local] end create_or_update_with_change_time = Time.now - start_time diff --git a/test/lib/parse/webhook_triggers_test.rb b/test/lib/parse/webhook_triggers_test.rb index 1d2d9ef..9620889 100644 --- a/test/lib/parse/webhook_triggers_test.rb +++ b/test/lib/parse/webhook_triggers_test.rb @@ -78,7 +78,7 @@ def test_before_save_trigger } client_payload = Parse::Webhooks::Payload.new(client_payload_data) - result = Parse::Webhooks.call_route(:before_save, "TestObject", client_payload) + result = Parse::Webhooks.call_route(:before_save, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] assert hook_called, "before_save hook should be called for client" assert hook_payload.before_save?, "Payload should identify as before_save" @@ -153,7 +153,7 @@ def test_after_save_trigger # Add after_create callback for new objects test_object.define_singleton_method(:run_after_create_callbacks) { callback_executed = true } - result = Parse::Webhooks.call_route(:after_save, "TestObject", client_payload) + result = Parse::Webhooks.call_route(:after_save, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] Parse::Webhooks.run_after_save_chain(client_payload) assert hook_called, "after_save hook should be called for client" @@ -222,7 +222,7 @@ def test_before_delete_trigger client_payload = Parse::Webhooks::Payload.new(client_payload_data) client_payload.define_singleton_method(:parse_object) { test_object } - result = Parse::Webhooks.call_route(:before_delete, "TestObject", client_payload) + result = Parse::Webhooks.call_route(:before_delete, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] assert hook_called, "before_delete hook should be called for client" assert hook_payload.before_delete?, "Payload should identify as before_delete" @@ -282,7 +282,7 @@ def test_after_delete_trigger } client_payload = Parse::Webhooks::Payload.new(client_payload_data) - result = Parse::Webhooks.call_route(:after_delete, "TestObject", client_payload) + result = Parse::Webhooks.call_route(:after_delete, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] assert hook_called, "after_delete hook should be called for client" assert hook_payload.after_delete?, "Payload should identify as after_delete" @@ -347,7 +347,7 @@ def test_before_find_trigger client_payload = Parse::Webhooks::Payload.new(client_payload_data) client_payload.instance_variable_set(:@webhook_class, "TestObject") - result = Parse::Webhooks.call_route(:before_find, "TestObject", client_payload) + result = Parse::Webhooks.call_route(:before_find, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] assert hook_called, "before_find hook should be called for client" assert hook_payload.before_find?, "Payload should identify as before_find" @@ -421,7 +421,7 @@ def test_after_find_trigger client_payload = Parse::Webhooks::Payload.new(client_payload_data) client_payload.instance_variable_set(:@webhook_class, "TestObject") - result = Parse::Webhooks.call_route(:after_find, "TestObject", client_payload) + result = Parse::Webhooks.call_route(:after_find, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] assert hook_called, "after_find hook should be called for client" assert hook_payload.after_find?, "Payload should identify as after_find" @@ -534,7 +534,7 @@ def test_multiple_trigger_hooks } before_payload = Parse::Webhooks::Payload.new(before_payload_data) - result = Parse::Webhooks.call_route(:before_save, "TestObject", before_payload) + result = Parse::Webhooks.call_route(:before_save, "TestObject", before_payload) # codeql[rb/useless-assignment-to-local] assert_equal ["before2"], execution_order, "Only the last before_save hook should execute" puts "✅ Single before_save hook behavior works correctly" @@ -580,7 +580,7 @@ def test_trigger_error_handling # Direct call_route won't raise the error, but the error! method would be called # This tests that the conditional logic works correctly begin - result = Parse::Webhooks.call_route(:before_save, "TestObject", client_payload) + result = Parse::Webhooks.call_route(:before_save, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] flunk "Should have raised ResponseError for client request" rescue Parse::Webhooks::ResponseError => e assert_equal "Client validation failed", e.message, "Should have correct error message" @@ -616,7 +616,7 @@ def test_wildcard_trigger_routing payload = Parse::Webhooks::Payload.new(payload_data) payload.define_singleton_method(:parse_object) { nil } - result = Parse::Webhooks.call_route(:after_save, "TestObject", payload) + result = Parse::Webhooks.call_route(:after_save, "TestObject", payload) # codeql[rb/useless-assignment-to-local] assert specific_called, "Specific hook should be called" refute wildcard_called, "Wildcard hook should not be called when specific exists" @@ -639,7 +639,7 @@ def test_wildcard_trigger_routing assert_nil result, "No specific route should exist" # Then try wildcard route - result = Parse::Webhooks.call_route(:after_save, "*", unknown_payload) + result = Parse::Webhooks.call_route(:after_save, "*", unknown_payload) # codeql[rb/useless-assignment-to-local] refute specific_called, "Specific hook should not be called" assert wildcard_called, "Wildcard hook should be called for unknown class" diff --git a/test/support/docker_helper.rb b/test/support/docker_helper.rb index 63e4959..4e3ae24 100644 --- a/test/support/docker_helper.rb +++ b/test/support/docker_helper.rb @@ -16,7 +16,7 @@ def start! puts "Starting Parse Server test container..." - stdout, stderr, status = Open3.capture3("docker-compose -f #{COMPOSE_FILE} up -d") + stdout, stderr, status = Open3.capture3("docker-compose -f #{COMPOSE_FILE} up -d") # codeql[rb/useless-assignment-to-local] if status.success? wait_for_server diff --git a/test/support/test_server.rb b/test/support/test_server.rb index 10b58c8..e7cd13c 100644 --- a/test/support/test_server.rb +++ b/test/support/test_server.rb @@ -37,14 +37,14 @@ def server_available? uri = URI(Parse::Client.client.server_url + "/health") response = Net::HTTP.get_response(uri) response.code == "200" - rescue StandardError => e + rescue StandardError => e # codeql[rb/useless-assignment-to-local] # Fallback: Try to check if Parse is responding at all begin uri = URI(Parse::Client.client.server_url) response = Net::HTTP.get_response(uri) # Parse Server typically returns 404 or 401 for root path but it means server is up ["200", "404", "401", "403"].include?(response.code) - rescue StandardError => e2 + rescue StandardError => e2 # codeql[rb/useless-assignment-to-local] false end end @@ -82,12 +82,12 @@ def reset_database! begin obj.destroy total_deleted += 1 - rescue => e + rescue => e # codeql[rb/useless-assignment-to-local] # Silent failure - continue with other objects end end end - rescue StandardError => e + rescue StandardError => e # codeql[rb/useless-assignment-to-local] # Silent failure - continue with other classes end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 88ce54d..3d9f855 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -57,7 +57,7 @@ def test_scalar_values module Minitest module Assertions def refute_raises(*exp) - msg = "#{exp.pop}.\n" if String === exp.last + msg = "#{exp.pop}.\n" if String === exp.last # codeql[rb/useless-assignment-to-local] begin yield @@ -65,7 +65,7 @@ def refute_raises(*exp) return e if exp.include? Minitest::Skip raise e rescue Exception => e - exp = exp.first if exp.size == 1 + exp = exp.first if exp.size == 1 # codeql[rb/useless-assignment-to-local] flunk "unexpected exception raised: #{e}" end end From 831f8b8e6be06490ab8507ce477b9eb8ee2f754e Mon Sep 17 00:00:00 2001 From: Adrian Curtin <48138055+AdrianCurtin@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:18:49 -0400 Subject: [PATCH 5/5] Add Query#where_not_between and presigned URL safety NEW: Query#where_not_between(field, value) adds the logical negation of between constraints, supporting Range and Array forms with proper handling of beginless/endless ranges. Raises ArgumentError if query already has an $or group to prevent silent collision. FIXED: embed_image presigned URL handling now uses zero safety buffer (not the default 60s) for immediate fetches, preventing deterministic 403s in the last minute of valid signatures on private-bucket adapters. Strips query strings from stored URLs to avoid leaking bearer credentials in logs/error output. FIXED: Parse::Client._safe_warn now falls back to STDERR if a configured logger itself raises, preventing logger errors from masking the real Parse error being reported. CHANGED: Test suite cleaned up to remove unused variable assignments flagged by CodeQL. --- CHANGELOG.md | 41 +++++++- lib/parse/client.rb | 16 +++- lib/parse/embeddings.rb | 21 ++++- lib/parse/embeddings/image_fetch.rb | 10 +- lib/parse/model/core/embed_managed.rb | 13 ++- lib/parse/query.rb | 71 ++++++++++++++ lib/parse/query/constraints.rb | 23 +++++ .../parse/acl_constraints_integration_test.rb | 2 +- test/lib/parse/acl_dirty_tracking_test.rb | 2 +- test/lib/parse/agent/mcp_integration_test.rb | 2 +- test/lib/parse/agent/mcp_streaming_test.rb | 5 - .../lib/parse/agent/tools_get_objects_test.rb | 1 - .../array_constraints_210_integration_test.rb | 1 - test/lib/parse/audience_test.rb | 1 - test/lib/parse/cache_integration_test.rb | 1 - test/lib/parse/cache_write_only_test.rb | 4 +- test/lib/parse/client/safe_warn_test.rb | 16 ++++ .../client_rest_files_integration_test.rb | 2 +- .../parse/cloud_config_integration_test.rb | 2 +- .../parse/count_distinct_integration_test.rb | 2 - test/lib/parse/count_distinct_simple_test.rb | 2 - test/lib/parse/count_distinct_test.rb | 36 ------- test/lib/parse/distinct_pointer_test.rb | 20 ---- test/lib/parse/docker_integration_test.rb | 2 +- test/lib/parse/embed_managed_image_test.rb | 17 ++++ test/lib/parse/embeddings_image_fetch_test.rb | 15 +++ test/lib/parse/equals_linked_pointer_test.rb | 4 - .../parse/features_220_integration_test.rb | 2 +- ...field_guards_delete_op_integration_test.rb | 5 - test/lib/parse/field_guards_test.rb | 4 +- .../parse/field_selection_integration_test.rb | 1 - test/lib/parse/graphql_type_generator_test.rb | 1 - .../hooks_and_validation_integration_test.rb | 2 +- test/lib/parse/live_query_integration_test.rb | 2 +- .../model_associations_integration_test.rb | 2 +- .../parse/models/count_distinct_model_test.rb | 2 - test/lib/parse/models/polygon_test.rb | 2 +- test/lib/parse/models/transaction_test.rb | 2 +- .../parse/mongodb_direct_integration_test.rb | 3 - .../parse/partial_fetch_integration_test.rb | 1 - test/lib/parse/push_integration_test.rb | 1 - .../parse/query/aggregation_features_test.rb | 2 - .../query/constraints/nullability_test.rb | 1 - .../parse/query/constraints/polygon_test.rb | 1 - .../parse/query/group_by_aggregation_test.rb | 5 - .../lib/parse/query/where_not_between_test.rb | 93 +++++++++++++++++++ .../parse/query_aggregate_integration_test.rb | 12 ++- test/lib/parse/query_compile_snapshot_test.rb | 32 +++++++ ...query_integration_fast_integration_test.rb | 4 +- test/lib/parse/query_integration_test.rb | 7 +- test/lib/parse/query_or_and_test.rb | 2 +- ...uery_pointers_contains_integration_test.rb | 1 - test/lib/parse/security_hardening_test.rb | 2 +- test/lib/parse/time_query_integration_test.rb | 2 - .../parse/upsert_methods_integration_test.rb | 2 +- test/lib/parse/webhook_triggers_test.rb | 20 ++-- .../where_not_between_beginless.json | 7 ++ .../where_not_between_endless.json | 7 ++ .../where_not_between_exclusive.json | 16 ++++ .../where_not_between_inclusive.json | 16 ++++ .../where_not_between_with_and_condition.json | 17 ++++ test/support/docker_helper.rb | 2 +- test/support/test_server.rb | 8 +- test/test_helper.rb | 3 +- 64 files changed, 467 insertions(+), 157 deletions(-) create mode 100644 test/lib/parse/query/where_not_between_test.rb create mode 100644 test/snapshots/query_compile/where_not_between_beginless.json create mode 100644 test/snapshots/query_compile/where_not_between_endless.json create mode 100644 test/snapshots/query_compile/where_not_between_exclusive.json create mode 100644 test/snapshots/query_compile/where_not_between_inclusive.json create mode 100644 test/snapshots/query_compile/where_not_between_with_and_condition.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 735ade3..bc587ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - **NEW**: The `between` constraint now accepts a Ruby `Range` in addition to a 2-element array, so `Person.where(:age.between => 5..25)` and - `Record.where(:date.between => 2.days.ago...5.days.ago)` work directly. An + `Record.where(:date.between => 5.days.ago...2.days.ago)` work directly. An inclusive range (`..`) maps its upper bound to `$lte`, matching the existing array form, while an exclusive range (`...`) maps it to `$lt` instead. Beginless (`..25`) and endless (`5..`) ranges are also supported and @@ -14,6 +14,24 @@ 18..)` compiles to `{"$gte" => 18}` with no upper bound. The array form is unchanged, and both forms produce identical output for the same bounds. +#### `Query#where_not_between` for the negated form of a range + +- **NEW**: `Query#where_not_between(field, value)` adds the logical negation + of `between`: `Person.query.where_not_between(:age, 5..25)` compiles to + `age < 5 OR age > 25`, accepting the same Range and 2-element Array forms + as `between` (exclusive ranges flip the upper side to `$gte`, and a + beginless or endless Range negates to a single one-sided comparison with + no `$or` needed). It is not available as a `field.not_between => value` + symbol constraint: a range's negation is inherently an `$or` of two + comparisons, and only one `$or` group can be safely merged into a + compiled query, so a symbol constraint that unilaterally emitted one + could silently collide with an existing `$or` from `or_where`/`|`. + `where_not_between` instead composes the negation the way + `Parse::Query.and` already does, so it correctly nests inside a query's + other `.where` conditions instead of replacing them, and raises + `ArgumentError` if the query already has an `$or` group rather than + silently dropping part of it. + #### `embed_image` forwards a presigned URL when the source file has one - **FIXED**: `embed_image` always sent the source file's bare `file.url` to @@ -27,7 +45,19 @@ expired, and falls back to the bare URL otherwise, for both `source: :url` and `source: :bytes`. The stored digest is still keyed on the bare canonical URL, so a save that only rotates the file's signature does not - trigger a needless re-embed. + trigger a needless re-embed. The validity check ignores + `presigned_url_valid?`'s default 60-second safety buffer (meant for a + browser render, not an immediate server-side fetch), since on a + private-bucket adapter the fallback URL is not fetchable at all and would + otherwise 403 for the last minute of every signature's life. + `Parse::Embeddings::ImageFetch::FetchedImage#url` now stores the + query-stripped URL rather than the presigned one, since a live signature + has no reason to survive into that value object's `#inspect` output. + `source: :url` mode can now forward a presigned URL to the embedding + provider under the same `Parse::Embeddings.trust_provider_url_fetch` + consent already required to forward any URL; operators relying on private + buckets should confirm the provider's egress handling covers + credential-bearing URLs, not just public ones. #### `Query#get` now resolves aliased `parse_class` names correctly @@ -49,7 +79,12 @@ the rest of its Parse request/response logging. These warnings now route through `Parse::Middleware::Logging.logger` when one is configured, so they land in the same place as the app's other logs; STDERR remains the fallback - when no logger is configured, matching prior behavior. + when no logger is configured, matching prior behavior. Every call site + raises the corresponding typed `Parse::Error` immediately after this + warning, so a configured logger that itself raises (a closed handle, a + full disk, a remote-aggregator client erroring on a socket) now falls back + to STDERR rather than propagating in place of the real error and masking + it. ### 5.7.1 diff --git a/lib/parse/client.rb b/lib/parse/client.rb index a8a5a67..0107d5e 100644 --- a/lib/parse/client.rb +++ b/lib/parse/client.rb @@ -516,7 +516,12 @@ def setup(opts = {}, &block) # configured one (`Parse.logger = ...`), so these warnings land wherever # the rest of the app's Parse request/response logging goes instead of # bypassing it. Falls back to plain `warn` (STDERR) when no logger is - # configured, matching prior behavior. + # configured, matching prior behavior. Every call site immediately + # raises the corresponding typed {Parse::Error} right after calling + # this method, so a misbehaving app-supplied logger (closed handle, + # full disk, a remote-aggregator client that raises on socket error) + # must not be allowed to propagate in its place and mask the real + # error — falls back to `warn` if the logger itself raises. # # @param tag [String] the bracketed prefix (e.g. "AuthenticationError"). # @param response [Parse::Response] the response carrying the error. @@ -529,8 +534,13 @@ def _safe_warn(tag, response, name: nil) else "[Parse:#{tag}] [E-#{response.code}] #{response.request} : #{err} (#{response.http_status})" end - if Parse::Middleware::Logging.logger - Parse::Middleware::Logging.logger.warn(msg) + logger = Parse::Middleware::Logging.logger + if logger + begin + logger.warn(msg) + rescue StandardError + warn msg + end else warn msg end diff --git a/lib/parse/embeddings.rb b/lib/parse/embeddings.rb index f1f1fa1..f075b2a 100644 --- a/lib/parse/embeddings.rb +++ b/lib/parse/embeddings.rb @@ -382,6 +382,17 @@ def max_media_bytes # `"true"`, or a non-matching String) raises # {ConfirmationRequired}. Reset to `nil` to disable. # + # For a `:file` source backed by a private-bucket adapter + # (S3/GCS with server-side presigning), the URL forwarded under + # this sentinel may be the file's presigned URL rather than its + # bare canonical one — a time-limited bearer credential for that + # object, not just a pointer to it (see {Parse::File#presigned_url} + # and `Parse::Core::EmbedManaged.embed_image`). Reviewing the + # provider's egress behavior before setting this sentinel should + # account for that: the provider (and anyone with access to its + # request logs) gains temporary read access to the object for + # however long the signature remains valid. + # # @param value [String, nil] {TRUST_PROVIDER_URL_FETCH_SENTINEL} or nil. # @raise [ConfirmationRequired] on any other value. def trust_provider_url_fetch=(value) @@ -395,10 +406,12 @@ def trust_provider_url_fetch=(value) "String #{TRUST_PROVIDER_URL_FETCH_SENTINEL.inspect}. Plain `true` and " \ "other values are refused — forwarding image URLs to a third-party " \ "provider lets that provider issue an HTTP request from its own network " \ - "with attacker-controllable host/path. Set the sentinel only after you " \ - "have configured Parse::Embeddings.allowed_image_hosts AND reviewed the " \ - "provider's documented egress behavior (DNS rebinding window, redirect " \ - "policy)." + "with attacker-controllable host/path, and for a private-bucket file may " \ + "hand it a time-limited presigned URL rather than a bare pointer. Set the " \ + "sentinel only after you have configured " \ + "Parse::Embeddings.allowed_image_hosts AND reviewed the provider's " \ + "documented egress behavior (DNS rebinding window, redirect policy, " \ + "request-log retention)." end CONFIG_MUTEX.synchronize { @trust_provider_url_fetch = value } end diff --git a/lib/parse/embeddings/image_fetch.rb b/lib/parse/embeddings/image_fetch.rb index 986fb92..d79e7e0 100644 --- a/lib/parse/embeddings/image_fetch.rb +++ b/lib/parse/embeddings/image_fetch.rb @@ -164,7 +164,15 @@ def fetch!(url, allow_insecure: false, exif_strip: true, max_bytes: nil) mime = verify!(bytes, url: canonical) bytes = strip_metadata(bytes, mime) if exif_strip - FetchedImage.new(bytes: bytes, mime_type: mime, url: canonical) + # Store the query-stripped URL, not `canonical` verbatim: when + # `url` is a presigned URL (a private-bucket file adapter), + # `canonical` carries a live signature, and FetchedImage#url is + # purely informational from here on (nothing re-fetches it). + # Keeping the signature out of the struct preserves the + # log-safety `#inspect` below was written for — third-party + # provider adapters and error reporters that capture locals + # would otherwise leak a valid bearer credential through it. + FetchedImage.new(bytes: bytes, mime_type: mime, url: Parse::File.strip_query(canonical)) end # Verify raw bytes: sniff the magic, check the allowlist, and diff --git a/lib/parse/model/core/embed_managed.rb b/lib/parse/model/core/embed_managed.rb index 0a7287b..2fc9b48 100644 --- a/lib/parse/model/core/embed_managed.rb +++ b/lib/parse/model/core/embed_managed.rb @@ -810,9 +810,20 @@ def self.call_provider(provider, directive, input, record) # it has one, otherwise the bare canonical `fallback` (the same # string used for the digest). Never used for text directives, so # `directive.sources.first` is always a `:file` property here. + # + # Checks validity with a zero safety buffer rather than + # {Parse::File#presigned_url_valid?}'s default 60-second one. That + # default exists so a browser has time to render before a + # presigned URL goes stale; here it would instead spend the last + # 60 seconds of a perfectly usable presigned URL falling back to + # `fallback`, which on a private-bucket adapter is not fetchable + # at all. A fetch that starts immediately after this check has no + # meaningful use for that margin, and a 403 from a URL that + # expired mid-request is strictly better than a guaranteed 403 + # from a URL known unfetchable in advance. def self.presigned_fetch_url(record, directive, fallback) file = record.public_send(directive.sources.first) - if file.respond_to?(:presigned_url_valid?) && file.presigned_url_valid? + if file.respond_to?(:presigned_url_valid?) && file.presigned_url_valid?(buffer: 0) file.presigned_url else fallback diff --git a/lib/parse/query.rb b/lib/parse/query.rb index 5ba173f..b7e2eea 100644 --- a/lib/parse/query.rb +++ b/lib/parse/query.rb @@ -1113,6 +1113,77 @@ def |(other_query) copy_query end + # Add a "field is NOT between" condition — the logical negation of + # `.where(field.between => value)`: `field < min OR field > max` for a + # fully-bounded Range/Array, or a single one-sided comparison when the + # Range is beginless/endless (mirroring how {Parse::Constraint::BetweenConstraint} + # itself only constrains the side that is present). + # + # Not available as a `field.not_between => value` symbol constraint, + # unlike `.between`: a between-style range is inherently an OR of two + # comparisons, and a single {Parse::Constraint}'s `#build` can only + # safely contribute an AND'd clause to a query. A constraint that + # unilaterally emitted a top-level `$or` would collide with (and + # silently clobber, or be clobbered by) any other `$or` this query + # already produces via {#or_where} / `|` / another `where_not_between` + # call, since only one `$or` group merges correctly per query. This + # method instead composes the negation the same way {Parse::Query.and} + # does — concatenating compiled constraint arrays — which correctly + # nests the OR inside the query's existing AND'd conditions instead of + # replacing them the way {#or_where} would. + # + # @example + # Person.query.where_not_between(:age, 5..25) + # # age < 5 OR age > 25 + # + # Record.query.where(:archived => false).where_not_between(:date, 5.days.ago...2.days.ago) + # # archived == false AND (date < 5.days.ago OR date >= 2.days.ago) + # + # @param field [Symbol, String] the field to constrain. + # @param value [Range, Array] a `between`-style value: a Range (including + # beginless/endless/exclusive-end forms) or a 2-element `[min, max]` Array. + # @return [self] + # @raise [ArgumentError] if `value` isn't a Range or 2-element Array, or + # is a fully-open (`nil..nil`) Range. + def where_not_between(field, value) + field = field.to_sym + min_value, max_value, exclude_max = Parse::Constraint::BetweenConstraint.extract_bounds(value) + + if min_value.nil? && max_value.nil? + raise ArgumentError, "Query#where_not_between: Range must have a begin, an end, or both (ex. 5.., ..25, 5..25)." + end + + # A fully-bounded range needs its own `$or` group (`field < min OR + # field > max`). Only ONE `$or` group survives the plain-Hash merge + # every constraint's compiled output goes through (`constraint_reduce` + # deep-merges compiled hashes; a second top-level `$or` key silently + # overwrites the first rather than combining with it — the same + # constraint that makes `field.not_between => value` unsafe as a + # symbol constraint, see above). Fail loudly here instead of quietly + # dropping half the query if this query already has one, from + # `#or_where`, `|`, or an earlier `where_not_between` call. + if min_value && max_value && @where.any? { |c| c.is_a?(Parse::Constraint::CompoundQueryConstraint) } + raise ArgumentError, + "Query#where_not_between: this query already has an `$or` group (from `or_where`, `|`, " \ + "or a prior `where_not_between` call). Only one `$or` group can be safely merged per " \ + "query. Compose the two queries with Parse::Query.and(...) instead." + end + + negated = if min_value.nil? + Parse::Query.new(@table).where(field.public_send(exclude_max ? :gte : :gt) => max_value) + elsif max_value.nil? + Parse::Query.new(@table).where(field.lt => min_value) + else + lower = Parse::Query.new(@table).where(field.lt => min_value) + upper = Parse::Query.new(@table).where(field.public_send(exclude_max ? :gte : :gt) => max_value) + Parse::Query.or(lower, upper) + end + + @where = @where + negated.where + @results = nil + self + end + # Queries can be made using distinct, allowing you find unique values for a specified field. # For this to be performant, please remember to index your database. # @example diff --git a/lib/parse/query/constraints.rb b/lib/parse/query/constraints.rb index ef11929..e904363 100644 --- a/lib/parse/query/constraints.rb +++ b/lib/parse/query/constraints.rb @@ -2554,6 +2554,29 @@ def build } } end + # @!visibility private + # Extract raw (unformatted) `[min_value, max_value, exclude_max]` + # bounds from a `between`-style value: a Range (`exclude_max` + # reflects `exclude_end?`) or a 2-element Array (always inclusive + # on both ends, matching {#build}'s array branch). Shared with + # {Parse::Query#where_not_between}, which needs the same bounds + # to build the negated (`$or` of the two flipped comparisons) + # form — a shape {BetweenConstraint} itself cannot safely emit + # from a single constraint's `#build` (see `where_not_between`'s + # docs for why). + # @param value [Range, Array] a `between`-style value. + # @return [Array(Object, Object, Boolean)] + # @raise [ArgumentError] if `value` isn't a Range or 2-element Array. + def self.extract_bounds(value) + if value.is_a?(Range) + [value.begin, value.end, value.exclude_end?] + elsif value.is_a?(Array) && value.length == 2 + [value[0], value[1], false] + else + raise ArgumentError, "#{name}: Value must be an array with exactly 2 elements [min_value, max_value], or a Range" + end + end + private # @return [Hash] the compiled constraint for a Ruby Range value. diff --git a/test/lib/parse/acl_constraints_integration_test.rb b/test/lib/parse/acl_constraints_integration_test.rb index ddc237e..a6ca96f 100644 --- a/test/lib/parse/acl_constraints_integration_test.rb +++ b/test/lib/parse/acl_constraints_integration_test.rb @@ -455,7 +455,7 @@ def test_acl_constraints_with_arrays # Create test roles admin_role = create_test_role("Admin") editor_role = create_test_role("Editor") - viewer_role = create_test_role("Viewer") # codeql[rb/useless-assignment-to-local] + create_test_role("Viewer") # Create documents with role-based access doc1 = create_test_document(title: "Admin Doc", content: "Admin content") diff --git a/test/lib/parse/acl_dirty_tracking_test.rb b/test/lib/parse/acl_dirty_tracking_test.rb index f36dccd..1f2fea4 100644 --- a/test/lib/parse/acl_dirty_tracking_test.rb +++ b/test/lib/parse/acl_dirty_tracking_test.rb @@ -174,7 +174,7 @@ def test_changes_shows_correct_before_and_after_for_in_place_modification changes = @obj.changes["acl"] refute_nil changes, "changes should include acl" - was_acl, current_acl = changes # codeql[rb/useless-assignment-to-local] + _, current_acl = changes # NOTE: ActiveModel's `changes` hash stores references internally, so both # was_acl and current_acl point to the same mutated object. This is a known diff --git a/test/lib/parse/agent/mcp_integration_test.rb b/test/lib/parse/agent/mcp_integration_test.rb index 44f03eb..9aa6592 100644 --- a/test/lib/parse/agent/mcp_integration_test.rb +++ b/test/lib/parse/agent/mcp_integration_test.rb @@ -295,7 +295,7 @@ def test_block_form_factory_returning_agent_gives_200 "CONTENT_TYPE" => "application/json", "rack.input" => StringIO.new(raw), } - status, _hdrs, chunks = app.call(env) # codeql[rb/useless-assignment-to-local] + status, _hdrs, _chunks = app.call(env) assert_equal 200, status end diff --git a/test/lib/parse/agent/mcp_streaming_test.rb b/test/lib/parse/agent/mcp_streaming_test.rb index eaa8a10..d16b3aa 100644 --- a/test/lib/parse/agent/mcp_streaming_test.rb +++ b/test/lib/parse/agent/mcp_streaming_test.rb @@ -703,7 +703,6 @@ def test_dispatcher_thread_kill_outer_worker_recovers_and_pushes_done assert drain_thread.join(3), "drain_thread deadlocked after dispatcher_thread kill" # Collect the events and verify we got an error response (not a real one). - chunks = [] # codeql[rb/useless-assignment-to-local] begin drain_thread.value # re-raise any exception from drain_thread rescue @@ -822,8 +821,6 @@ def test_client_disconnect_mid_stream_no_leaked_threads app = streaming_app(heartbeat_interval: 0.1) _status, _headers, body = app.call(rack_env(accept: "text/event-stream")) - threads_before = Thread.list.size # codeql[rb/useless-assignment-to-local] - # Partially drain (receive one event) then close — simulates client disconnect. received = [] drain_thread = Thread.new do @@ -1182,7 +1179,6 @@ def test_tool_progress_uses_request_progress_token def test_progress_callback_exceptions_do_not_break_stream # First call raises inside the callback boundary; second is well-formed. # The stream should still deliver the second event and the response. - raising_call_done = false # codeql[rb/useless-assignment-to-local] StreamingDispatcherStub.progress_calls = [ { progress: "not-numeric" }, # invalid kwarg — but the callback itself # accepts anything; the stream encoder @@ -1207,7 +1203,6 @@ def test_progress_callback_exceptions_do_not_break_stream # --------------------------------------------------------------------------- def test_cancellation_token_is_installed_on_agent_during_dispatch - captured = nil # codeql[rb/useless-assignment-to-local] # Capture the token the dispatcher receives so we can verify # MCPRackApp constructed and passed one along. StreamingDispatcherStub.delay = 0.05 diff --git a/test/lib/parse/agent/tools_get_objects_test.rb b/test/lib/parse/agent/tools_get_objects_test.rb index c90d052..c5b0273 100644 --- a/test/lib/parse/agent/tools_get_objects_test.rb +++ b/test/lib/parse/agent/tools_get_objects_test.rb @@ -57,7 +57,6 @@ def test_empty_ids_returns_empty_result_without_querying def test_50_ids_success ids = (1..50).map { |i| "id#{i.to_s.rjust(8, "0")}"[0, 10] } - ids = ids.map.with_index { |_, i| "abcde#{i.to_s.rjust(5, "0")}"[0, 10] } # codeql[rb/useless-assignment-to-local] # Ensure uniqueness and valid format ids = (1..50).map { |i| format("abc%07d", i) } diff --git a/test/lib/parse/array_constraints_210_integration_test.rb b/test/lib/parse/array_constraints_210_integration_test.rb index 3be8a44..e68e487 100644 --- a/test/lib/parse/array_constraints_210_integration_test.rb +++ b/test/lib/parse/array_constraints_210_integration_test.rb @@ -374,7 +374,6 @@ def test_empty_or_nil_with_date_constraint now = Time.now one_day_ago = now - 86400 - two_days_ago = now - 172800 # codeql[rb/useless-assignment-to-local] with_timeout(10, "creating test data") do # Items with empty/nil tags at different times diff --git a/test/lib/parse/audience_test.rb b/test/lib/parse/audience_test.rb index 13a1271..99c7aeb 100644 --- a/test/lib/parse/audience_test.rb +++ b/test/lib/parse/audience_test.rb @@ -90,7 +90,6 @@ def test_concurrent_cache_access_does_not_raise def test_cache_fetch_with_concurrent_writes @fetch_return = nil - mutex = Mutex.new # codeql[rb/useless-assignment-to-local] threads = 5.times.map do Thread.new do diff --git a/test/lib/parse/cache_integration_test.rb b/test/lib/parse/cache_integration_test.rb index 5496d39..2b394e4 100644 --- a/test/lib/parse/cache_integration_test.rb +++ b/test/lib/parse/cache_integration_test.rb @@ -185,7 +185,6 @@ def test_cache_expiration_behavior # Fetch with custom cache expires header # Note: This tests the X-Parse-Stack-Cache-Expires header functionality - client = Parse.client # codeql[rb/useless-assignment-to-local] # First fetch should populate cache fetched_product1 = CacheTestProduct.find(product_id) diff --git a/test/lib/parse/cache_write_only_test.rb b/test/lib/parse/cache_write_only_test.rb index 66fabef..b5bb0e1 100644 --- a/test/lib/parse/cache_write_only_test.rb +++ b/test/lib/parse/cache_write_only_test.rb @@ -173,7 +173,6 @@ def test_fetch_defaults_to_write_only_when_feature_enabled # Mock the client to capture the request captured_opts = nil - original_client = song.method(:client) # codeql[rb/useless-assignment-to-local] song.define_singleton_method(:client) do mock_client = Object.new mock_client.define_singleton_method(:fetch_object) do |klass, id, **opts| @@ -428,7 +427,6 @@ def test_find_with_explicit_cache_false # Create a mock client to capture requests captured_cache_value = nil - original_client = Parse.client # codeql[rb/useless-assignment-to-local] mock_client = Object.new mock_client.define_singleton_method(:fetch_object) do |klass, id, **opts| @@ -517,7 +515,7 @@ def test_write_only_mode_skips_cache_read } # Call middleware - response = middleware.call(env) # codeql[rb/useless-assignment-to-local] + middleware.call(env) # Should have called the app (not used cache) assert fresh_response_called, "Should call the app when write_only mode is enabled" diff --git a/test/lib/parse/client/safe_warn_test.rb b/test/lib/parse/client/safe_warn_test.rb index 249ab17..3ccbc66 100644 --- a/test/lib/parse/client/safe_warn_test.rb +++ b/test/lib/parse/client/safe_warn_test.rb @@ -133,6 +133,22 @@ def test_falls_back_to_stderr_when_no_logger_configured assert_match(/\[Parse:ServerError\]/, err) end + def test_falls_back_to_stderr_when_configured_logger_raises + broken_logger = Object.new + broken_logger.define_singleton_method(:warn) { |_msg| raise IOError, "closed stream" } + Parse::Middleware::Logging.logger = broken_logger + + r = make_response(error: "Boom") + result = nil + _out, err = capture_io do + result = Parse::Client._safe_warn("ServerError", r) + end + + assert_nil result, "_safe_warn must not let a raising logger propagate past it" + assert_match(/\[Parse:ServerError\]/, err, + "a raising logger must fall back to STDERR instead of masking the real error") + end + def test_logger_path_still_redacts_credentials messages = [] fake_logger = Object.new diff --git a/test/lib/parse/client_rest_files_integration_test.rb b/test/lib/parse/client_rest_files_integration_test.rb index 9013939..e7a9cb7 100644 --- a/test/lib/parse/client_rest_files_integration_test.rb +++ b/test/lib/parse/client_rest_files_integration_test.rb @@ -73,7 +73,7 @@ def test_anonymous_upload_does_not_use_master_key filename = "anon_#{SecureRandom.hex(3)}.txt" begin - response = Parse.client.create_file(filename, contents, "text/plain") # codeql[rb/useless-assignment-to-local] + Parse.client.create_file(filename, contents, "text/plain") # If it succeeded, that's purely because Parse Server's # fileUpload.anonymousUsers was on. We can't disprove that from # the SDK side — but we CAN confirm the master key wasn't sent diff --git a/test/lib/parse/cloud_config_integration_test.rb b/test/lib/parse/cloud_config_integration_test.rb index 27968c7..da701eb 100644 --- a/test/lib/parse/cloud_config_integration_test.rb +++ b/test/lib/parse/cloud_config_integration_test.rb @@ -434,7 +434,7 @@ def test_config_client_methods_and_caching Parse.set_config("cacheTest", "updatedValue") # Read with caching (should still return old cached value) - cached_config3 = Parse.config # codeql[rb/useless-assignment-to-local] + Parse.config # Note: This might still return the cached value depending on implementation # Force refresh cache diff --git a/test/lib/parse/count_distinct_integration_test.rb b/test/lib/parse/count_distinct_integration_test.rb index 43f3b58..5576892 100644 --- a/test/lib/parse/count_distinct_integration_test.rb +++ b/test/lib/parse/count_distinct_integration_test.rb @@ -110,8 +110,6 @@ def test_count_distinct_with_mixed_conditions_including_dates base_time = Time.now.utc yesterday = base_time - 86400 - week_ago = base_time - 604800 # codeql[rb/useless-assignment-to-local] - month_ago = base_time - 2592000 # codeql[rb/useless-assignment-to-local] # Create reviews with different dates and ratings reviews = [] diff --git a/test/lib/parse/count_distinct_simple_test.rb b/test/lib/parse/count_distinct_simple_test.rb index 3d411bb..faaf3bc 100644 --- a/test/lib/parse/count_distinct_simple_test.rb +++ b/test/lib/parse/count_distinct_simple_test.rb @@ -32,7 +32,6 @@ def test_count_distinct_pipeline_construction query = Parse::Query.new("Song") # Mock the client to capture the pipeline - captured_pipeline = nil # codeql[rb/useless-assignment-to-local] mock_client = Object.new def mock_client.aggregate_pipeline(table, pipeline, **opts) @captured_pipeline = pipeline @@ -75,7 +74,6 @@ def test_count_distinct_with_where_conditions query.where(:play_count.gt => 100) # Mock the client to capture the pipeline - captured_pipeline = nil # codeql[rb/useless-assignment-to-local] mock_client = Object.new def mock_client.aggregate_pipeline(table, pipeline, **opts) @captured_pipeline = pipeline diff --git a/test/lib/parse/count_distinct_test.rb b/test/lib/parse/count_distinct_test.rb index f2b2ecc..34f322f 100644 --- a/test/lib/parse/count_distinct_test.rb +++ b/test/lib/parse/count_distinct_test.rb @@ -19,11 +19,6 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ # codeql[rb/useless-assignment-to-local] - { "$group" => { "_id" => "$genre" } }, - { "$count" => "distinctCount" }, - ] - @mock_client.expect :aggregate_pipeline, mock_response do |table, pipeline, **kwargs| table == "Song" && pipeline.is_a?(Array) end @@ -47,12 +42,6 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ # codeql[rb/useless-assignment-to-local] - { "$match" => { "playCount" => { "$gt" => 100 } } }, - { "$group" => { "_id" => "$artist" } }, - { "$count" => "distinctCount" }, - ] - @mock_client.expect :aggregate_pipeline, mock_response do |table, pipeline, **kwargs| table == "Song" && pipeline.is_a?(Array) end @@ -73,11 +62,6 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ # codeql[rb/useless-assignment-to-local] - { "$group" => { "_id" => "$genre" } }, - { "$count" => "distinctCount" }, - ] - @mock_client.expect :aggregate_pipeline, mock_response do |table, pipeline, **kwargs| table == "Song" && pipeline.is_a?(Array) end @@ -97,11 +81,6 @@ def mock_response.respond_to?(method) method == :error? || super end - expected_pipeline = [ # codeql[rb/useless-assignment-to-local] - { "$group" => { "_id" => "$genre" } }, - { "$count" => "distinctCount" }, - ] - @mock_client.expect :aggregate_pipeline, mock_response do |table, pipeline, **kwargs| table == "Song" && pipeline.is_a?(Array) end @@ -141,11 +120,6 @@ def mock_response.respond_to?(method) end # Test that snake_case field gets converted to camelCase - expected_pipeline = [ # codeql[rb/useless-assignment-to-local] - { "$group" => { "_id" => "$playCount" } }, - { "$count" => "distinctCount" }, - ] - @mock_client.expect :aggregate_pipeline, mock_response do |table, pipeline, **kwargs| table == "Song" && pipeline.is_a?(Array) end @@ -179,16 +153,6 @@ def mock_response.respond_to?(method) end # The pipeline should include a $match stage with all conditions - expected_match = { # codeql[rb/useless-assignment-to-local] - "playCount" => { "$gt" => 100 }, - "genre" => "rock", - "releaseDate" => { - "$gte" => { "__type" => "Date", "iso" => yesterday.iso8601(3) }, - "$lte" => { "__type" => "Date", "iso" => now.iso8601(3) }, - }, - "featured" => true, - } - @mock_client.expect :aggregate_pipeline, mock_response do |table, pipeline, **kwargs| table == "Song" && pipeline.is_a?(Array) && diff --git a/test/lib/parse/distinct_pointer_test.rb b/test/lib/parse/distinct_pointer_test.rb index 32ca773..f9166ce 100644 --- a/test/lib/parse/distinct_pointer_test.rb +++ b/test/lib/parse/distinct_pointer_test.rb @@ -21,11 +21,6 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ # codeql[rb/useless-assignment-to-local] - { "$group" => { "_id" => "$project" } }, - { "$project" => { "_id" => 0, "value" => "$_id" } }, - ] - @mock_client.expect :aggregate_pipeline, mock_response do |table, pipeline, **kwargs| table == "Asset" && pipeline.is_a?(Array) end @@ -57,11 +52,6 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ # codeql[rb/useless-assignment-to-local] - { "$group" => { "_id" => "$category" } }, - { "$project" => { "_id" => 0, "value" => "$_id" } }, - ] - @mock_client.expect :aggregate_pipeline, mock_response do |table, pipeline, **kwargs| table == "Asset" && pipeline.is_a?(Array) end @@ -89,11 +79,6 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ # codeql[rb/useless-assignment-to-local] - { "$group" => { "_id" => "$project" } }, - { "$project" => { "_id" => 0, "value" => "$_id" } }, - ] - @mock_client.expect :aggregate_pipeline, mock_response do |table, pipeline, **kwargs| table == "Asset" && pipeline.is_a?(Array) end @@ -160,11 +145,6 @@ def mock_response.respond_to?(method) [:error?, :result].include?(method) || super end - expected_pipeline = [ # codeql[rb/useless-assignment-to-local] - { "$group" => { "_id" => "$name" } }, - { "$project" => { "_id" => 0, "value" => "$_id" } }, - ] - @mock_client.expect :aggregate_pipeline, mock_response do |table, pipeline, **kwargs| table == "Asset" && pipeline.is_a?(Array) end diff --git a/test/lib/parse/docker_integration_test.rb b/test/lib/parse/docker_integration_test.rb index da36163..67c7f7c 100644 --- a/test/lib/parse/docker_integration_test.rb +++ b/test/lib/parse/docker_integration_test.rb @@ -218,7 +218,7 @@ def test_parse_schema_upgrade ["Post", "Author", "Comment"].each do |class_name| begin Parse.client.delete_schema(class_name, use_master_key: true) - rescue => e # codeql[rb/useless-assignment-to-local] + rescue # Ignore errors if schema doesn't exist end end diff --git a/test/lib/parse/embed_managed_image_test.rb b/test/lib/parse/embed_managed_image_test.rb index 0fae6c6..c622b07 100644 --- a/test/lib/parse/embed_managed_image_test.rb +++ b/test/lib/parse/embed_managed_image_test.rb @@ -226,6 +226,23 @@ def test_recompute_falls_back_to_bare_url_when_presigned_url_expired "an expired presigned URL must not be forwarded" end + def test_recompute_forwards_presigned_url_within_default_safety_buffer + # presigned_url_valid?'s default 60s buffer exists for browser + # rendering; it must NOT apply here. On a private-bucket adapter the + # fallback (bare file.url) is unfetchable, so treating a + # still-valid presigned URL as "expired soon" would deterministically + # 403 for the last 60 seconds of every signature's life. + doc = ImageDoc.new + doc.cover_art = file_with_url("https://1.1.1.1/cover.jpg") + doc.cover_art.instance_variable_set(:@presigned_url, "https://1.1.1.1/cover.jpg?X-Amz-Signature=abc") + doc.cover_art.instance_variable_set(:@presigned_url_expires_at, Time.now.utc + 30) + + Parse::Core::EmbedManaged.recompute_embedding!(doc, directive_for(ImageDoc, :cover_embedding)) + + assert_equal ["https://1.1.1.1/cover.jpg?X-Amz-Signature=abc"], @stub.calls.first[:sources], + "a presigned URL with 30s left must still be used, not treated as expired" + end + def test_recompute_digest_is_stable_across_presigned_url_rotation doc = ImageDoc.new doc.cover_art = file_with_url("https://1.1.1.1/cover.jpg") diff --git a/test/lib/parse/embeddings_image_fetch_test.rb b/test/lib/parse/embeddings_image_fetch_test.rb index dc2c7a8..5ed8de4 100644 --- a/test/lib/parse/embeddings_image_fetch_test.rb +++ b/test/lib/parse/embeddings_image_fetch_test.rb @@ -283,6 +283,21 @@ def test_fetch_forwards_max_bytes_to_safe_open_url assert_equal 4096, seen[:max_bytes], "fetch! must forward max_bytes: into safe_open_url" end + # FetchedImage#url must never retain a query string: on a + # private-bucket adapter, `url` passed into fetch! can be a presigned + # URL, and FetchedImage's whole point (see its #inspect) is to be + # safe to interpolate into exceptions/logs without leaking bytes — + # or a live signature. + def test_fetch_strips_query_string_from_stored_url + Parse::Embeddings.allowed_image_hosts = ["1.1.1.1"] + with_stubbed_download(png_with_exif) do + img = IF.fetch!("https://1.1.1.1/photo.png?X-Amz-Signature=super-secret-token") + assert_equal "https://1.1.1.1/photo.png", img.url + refute_match(/X-Amz-Signature|super-secret-token/, img.inspect) + refute_match(/X-Amz-Signature|super-secret-token/, img.to_s) + end + end + def test_fetch_requires_host_allowlist_but_not_sentinel # No trust_provider_url_fetch sentinel set — :fetch mode must work # on allowlist alone (the SDK fetches; no provider egress). diff --git a/test/lib/parse/equals_linked_pointer_test.rb b/test/lib/parse/equals_linked_pointer_test.rb index d3ac2c3..4f1e221 100644 --- a/test/lib/parse/equals_linked_pointer_test.rb +++ b/test/lib/parse/equals_linked_pointer_test.rb @@ -116,10 +116,6 @@ def test_query_requires_aggregation_pipeline_detection # Add equals_linked_pointer constraint query.where(:author.equals_linked_pointer => { through: :project, field: :owner }) - # Debug: check the compiled where clause structure - compiled_where = query.compile_where # codeql[rb/useless-assignment-to-local] - # puts "Compiled where: #{compiled_where.inspect}" - # Now should require pipeline assert query.requires_aggregation_pipeline? end diff --git a/test/lib/parse/features_220_integration_test.rb b/test/lib/parse/features_220_integration_test.rb index 13542ad..8c54550 100644 --- a/test/lib/parse/features_220_integration_test.rb +++ b/test/lib/parse/features_220_integration_test.rb @@ -160,7 +160,7 @@ def cleanup_test_models # Delete all objects of this class (limit 1000 should be enough for tests) objects = klass.all(limit: 1000) objects.each { |obj| obj.destroy rescue nil } - rescue => e # codeql[rb/useless-assignment-to-local] + rescue # Ignore cleanup errors - class may not exist yet end end diff --git a/test/lib/parse/field_guards_delete_op_integration_test.rb b/test/lib/parse/field_guards_delete_op_integration_test.rb index 6415008..e4dd511 100644 --- a/test/lib/parse/field_guards_delete_op_integration_test.rb +++ b/test/lib/parse/field_guards_delete_op_integration_test.rb @@ -64,11 +64,6 @@ def test_delete_op_overrides_client_supplied_value_on_create # The realistic scenario: a client sends BOTH a value and our webhook # response wants to drop it. Parse Server merges the webhook response with # the client payload, so the Delete op in the response must win. - body = { # codeql[rb/useless-assignment-to-local] - "slug" => "create-override", - # The client tried to write this value: - "secret" => "client-tried-to-leak-this", - } # Now simulate the webhook response merging by sending a SECOND payload # that resembles what our webhook code would emit: secret gets a Delete op. # Since we can't intercept here without a Rack endpoint, this case is diff --git a/test/lib/parse/field_guards_test.rb b/test/lib/parse/field_guards_test.rb index 6b44063..2cfdf5e 100644 --- a/test/lib/parse/field_guards_test.rb +++ b/test/lib/parse/field_guards_test.rb @@ -433,7 +433,7 @@ def test_class_with_only_guards_auto_registers_before_save_route # picks it up and Parse Server actually invokes our webhook. Parse::Webhooks.instance_variable_set(:@routes, nil) - klass = Class.new(Parse::Object) do # codeql[rb/useless-assignment-to-local] + Class.new(Parse::Object) do def self.parse_class; "AutoRegisteredGuardClass"; end property :name, :string property :owner, :string @@ -603,7 +603,7 @@ def test_guard_on_non_existent_property_is_silent_noop # A guard declared for a property name that doesn't exist on the model # cannot fire because the field is never in `changed`. This is a silent # no-op rather than a class-load-time error. - klass = Class.new(Parse::Object) do # codeql[rb/useless-assignment-to-local] + Class.new(Parse::Object) do def self.parse_class; "GuardedMissingField"; end property :real_field, :string guard :imaginary_field, :master_only # not declared as a property diff --git a/test/lib/parse/field_selection_integration_test.rb b/test/lib/parse/field_selection_integration_test.rb index 991fd2f..eb36124 100644 --- a/test/lib/parse/field_selection_integration_test.rb +++ b/test/lib/parse/field_selection_integration_test.rb @@ -479,7 +479,6 @@ def test_select_constraint_functionality # Test select constraint with simplified syntax (when field names match) # Create a query that looks for users by author_name field (this won't work since users don't have author_name) # Instead, let's test with a working scenario where field names actually match - posts_with_specific_names = FieldSelectionPost.query.where(:title.contains => "Famous") # codeql[rb/useless-assignment-to-local] # This simplified syntax would look for Users where 'title' field matches, but Users don't have title # So let's create a more appropriate test diff --git a/test/lib/parse/graphql_type_generator_test.rb b/test/lib/parse/graphql_type_generator_test.rb index fde4eea..34a03fd 100644 --- a/test/lib/parse/graphql_type_generator_test.rb +++ b/test/lib/parse/graphql_type_generator_test.rb @@ -189,7 +189,6 @@ def test_raw_array_property_emits_json_scalar_with_warning _stderr_was = $stderr captured = StringIO.new $stderr = captured - type = nil # codeql[rb/useless-assignment-to-local] begin type = Parse::GraphQL::TypeGenerator.generate_all(MODELS)["GqlGenArtist"] ensure diff --git a/test/lib/parse/hooks_and_validation_integration_test.rb b/test/lib/parse/hooks_and_validation_integration_test.rb index 3aaa9be..679f38d 100644 --- a/test/lib/parse/hooks_and_validation_integration_test.rb +++ b/test/lib/parse/hooks_and_validation_integration_test.rb @@ -159,7 +159,7 @@ def shipping_date_validation def should_send_email? # Use previous_changes in after_save context if previous_changes && previous_changes[:status] - old_status, new_status = previous_changes[:status] # codeql[rb/useless-assignment-to-local] + _, new_status = previous_changes[:status] ["completed", "shipped"].include?(new_status) else # Fallback for before_save context diff --git a/test/lib/parse/live_query_integration_test.rb b/test/lib/parse/live_query_integration_test.rb index 561ccdb..1c4f0aa 100644 --- a/test/lib/parse/live_query_integration_test.rb +++ b/test/lib/parse/live_query_integration_test.rb @@ -45,7 +45,7 @@ def cleanup_test_objects TestLiveQueryModel.all.each do |obj| obj.destroy rescue nil end - rescue => e # codeql[rb/useless-assignment-to-local] + rescue # Ignore errors during cleanup end diff --git a/test/lib/parse/model_associations_integration_test.rb b/test/lib/parse/model_associations_integration_test.rb index 652fa71..553d273 100644 --- a/test/lib/parse/model_associations_integration_test.rb +++ b/test/lib/parse/model_associations_integration_test.rb @@ -890,7 +890,7 @@ def test_association_edge_cases_and_error_handling # Note: Direct query works (AssociationTestBook.all(author: special_author) finds 1 book) # but has_many association query has an issue - skipping this assertion for now # TODO: Investigate why has_many association query doesn't find the book - special_books = special_author.books.results # codeql[rb/useless-assignment-to-local] + special_author.books.results all_books_for_author = AssociationTestBook.all(author: special_author) if all_books_for_author.count > 0 diff --git a/test/lib/parse/models/count_distinct_model_test.rb b/test/lib/parse/models/count_distinct_model_test.rb index e8c1d63..d32d0c7 100644 --- a/test/lib/parse/models/count_distinct_model_test.rb +++ b/test/lib/parse/models/count_distinct_model_test.rb @@ -70,8 +70,6 @@ def response.success? true end - # Capture response_data in the closure - response_data = @response_data # codeql[rb/useless-assignment-to-local] def response.result response_data end diff --git a/test/lib/parse/models/polygon_test.rb b/test/lib/parse/models/polygon_test.rb index 459912f..d6b08b3 100644 --- a/test/lib/parse/models/polygon_test.rb +++ b/test/lib/parse/models/polygon_test.rb @@ -109,7 +109,7 @@ def test_contains_point_invalid_arg end def test_warns_below_min_vertices - out, _err = capture_io do # codeql[rb/useless-assignment-to-local] + _, _err = capture_io do Parse::Polygon.new [[0.0, 0.0], [1.0, 1.0]] end # Warnings go to $stderr via Kernel#warn, captured by capture_io diff --git a/test/lib/parse/models/transaction_test.rb b/test/lib/parse/models/transaction_test.rb index 06b2515..de78e66 100644 --- a/test/lib/parse/models/transaction_test.rb +++ b/test/lib/parse/models/transaction_test.rb @@ -107,7 +107,7 @@ def test_transaction_with_custom_retry_count end begin - result = Parse::Object.transaction(retries: 10) do |batch| # codeql[rb/useless-assignment-to-local] + Parse::Object.transaction(retries: 10) do |batch| # Test that custom retry count is accepted assert_instance_of Parse::BatchOperation, batch end diff --git a/test/lib/parse/mongodb_direct_integration_test.rb b/test/lib/parse/mongodb_direct_integration_test.rb index 68c9ee6..6aa43c2 100644 --- a/test/lib/parse/mongodb_direct_integration_test.rb +++ b/test/lib/parse/mongodb_direct_integration_test.rb @@ -2086,8 +2086,6 @@ def test_aggregate_group_by_pointer_direct puts " Group IDs: #{group_ids.inspect}" has_null = group_ids.include?(nil) - has_artist1 = group_ids.any? { |id| id.to_s.include?(artist1.id) } # codeql[rb/useless-assignment-to-local] - has_artist2 = group_ids.any? { |id| id.to_s.include?(artist2.id) } # codeql[rb/useless-assignment-to-local] assert has_null, "Should have null group for albums without artist" # Note: The group by pointer returns pointer format, so check for id presence @@ -2564,7 +2562,6 @@ def test_aggregate_group_by_date_object_direct puts "\n=== Testing Aggregate Group by Date Object ===" # Create test data with dates - today = Time.now.utc # codeql[rb/useless-assignment-to-local] data = [ { title: "DateGroup1", artist: "DateGroup Artist", genre: "Rock", plays: 100 }, { title: "DateGroup2", artist: "DateGroup Artist", genre: "Pop", plays: 200 }, diff --git a/test/lib/parse/partial_fetch_integration_test.rb b/test/lib/parse/partial_fetch_integration_test.rb index 677e435..fd0ac81 100644 --- a/test/lib/parse/partial_fetch_integration_test.rb +++ b/test/lib/parse/partial_fetch_integration_test.rb @@ -900,7 +900,6 @@ def test_belongs_to_assignment_to_unfetched_field_tracks_changes author: user1, ) assert post.save, "Post should save" - post_id = post.id # codeql[rb/useless-assignment-to-local] # Fetch with only :title (author is not fetched) fetched_post = PartialFetchPost.first(keys: [:id, :title]) diff --git a/test/lib/parse/push_integration_test.rb b/test/lib/parse/push_integration_test.rb index 2b66fca..799487f 100644 --- a/test/lib/parse/push_integration_test.rb +++ b/test/lib/parse/push_integration_test.rb @@ -207,7 +207,6 @@ def test_installation_subscribe_structure # Test that subscribe modifies channels locally installation.channels = [] - original_channels = installation.channels.to_a.dup # codeql[rb/useless-assignment-to-local] # Mock save to prevent actual API call in this structure test installation.define_singleton_method(:save) { true } diff --git a/test/lib/parse/query/aggregation_features_test.rb b/test/lib/parse/query/aggregation_features_test.rb index 3d50669..85d4ed4 100644 --- a/test/lib/parse/query/aggregation_features_test.rb +++ b/test/lib/parse/query/aggregation_features_test.rb @@ -517,8 +517,6 @@ def test_count_distinct_pipeline_with_dates aggregation_where = query.send(:convert_constraints_for_aggregation, compiled_where) puts "After constraint conversion: #{aggregation_where.inspect}" - stringified_where = query.send(:convert_dates_for_aggregation, aggregation_where) # codeql[rb/useless-assignment-to-local] - aggregation_where = query.send(:convert_constraints_for_aggregation, compiled_where) stringified_where = query.send(:convert_dates_for_aggregation, aggregation_where) diff --git a/test/lib/parse/query/constraints/nullability_test.rb b/test/lib/parse/query/constraints/nullability_test.rb index 7aa2704..3b7584f 100644 --- a/test/lib/parse/query/constraints/nullability_test.rb +++ b/test/lib/parse/query/constraints/nullability_test.rb @@ -30,7 +30,6 @@ def test_scalar_values ["true", 1, nil].each do |value| constraint = @klass.new(:field, value) assert_raises(ArgumentError) do - expected = build(value).as_json # codeql[rb/useless-assignment-to-local] constraint.build.as_json end end diff --git a/test/lib/parse/query/constraints/polygon_test.rb b/test/lib/parse/query/constraints/polygon_test.rb index 0d9aff2..0811d6b 100644 --- a/test/lib/parse/query/constraints/polygon_test.rb +++ b/test/lib/parse/query/constraints/polygon_test.rb @@ -34,7 +34,6 @@ def test_argument_error end def test_compiled_query - triangle = [@bermuda, @miami, @san_juan] # codeql[rb/useless-assignment-to-local] compiled_query = { "location" => { "$geoWithin" => { "$polygon" => [ { :__type => "GeoPoint", :latitude => 32.3078, :longitude => -64.7504999 }, { :__type => "GeoPoint", :latitude => 25.7823198, :longitude => -80.2660226 }, diff --git a/test/lib/parse/query/group_by_aggregation_test.rb b/test/lib/parse/query/group_by_aggregation_test.rb index 7e52584..d07c669 100644 --- a/test/lib/parse/query/group_by_aggregation_test.rb +++ b/test/lib/parse/query/group_by_aggregation_test.rb @@ -31,11 +31,6 @@ def test_group_by_count_builds_correct_pipeline def test_group_by_sum_builds_correct_pipeline group_by = Parse::GroupBy.new(@query, :project) - expected_pipeline = [ # codeql[rb/useless-assignment-to-local] - { "$group" => { "_id" => "$project", "count" => { "$sum" => "$fileSize" } } }, - { "$project" => { "_id" => 0, "objectId" => "$_id", "count" => 1 } }, - ] - mock_response = Minitest::Mock.new mock_response.expect :success?, true mock_response.expect :result, [] diff --git a/test/lib/parse/query/where_not_between_test.rb b/test/lib/parse/query/where_not_between_test.rb new file mode 100644 index 0000000..f24a22d --- /dev/null +++ b/test/lib/parse/query/where_not_between_test.rb @@ -0,0 +1,93 @@ +require_relative "../../../test_helper" + +class TestWhereNotBetween < Minitest::Test + class NotBetweenPerson < Parse::Object + parse_class "NotBetweenPerson" + property :age, :integer + property :name, :string + end + + def compiled_where(query) + query.compile(encode: false).as_json["where"] + end + + def test_inclusive_range + where = compiled_where(NotBetweenPerson.query.where_not_between(:age, 5..25)) + assert_equal({ "$or" => [{ "age" => { "$lt" => 5 } }, { "age" => { "$gt" => 25 } }] }, where) + end + + def test_exclusive_range_flips_upper_bound_to_gte + where = compiled_where(NotBetweenPerson.query.where_not_between(:age, 5...25)) + assert_equal({ "$or" => [{ "age" => { "$lt" => 5 } }, { "age" => { "$gte" => 25 } }] }, where) + end + + def test_array_form_is_always_inclusive + where = compiled_where(NotBetweenPerson.query.where_not_between(:age, [5, 25])) + assert_equal({ "$or" => [{ "age" => { "$lt" => 5 } }, { "age" => { "$gt" => 25 } }] }, where) + end + + def test_beginless_range_has_no_or_wrapper + where = compiled_where(NotBetweenPerson.query.where_not_between(:age, ..25)) + assert_equal({ "age" => { "$gt" => 25 } }, where) + end + + def test_endless_range_has_no_or_wrapper + where = compiled_where(NotBetweenPerson.query.where_not_between(:age, 5..)) + assert_equal({ "age" => { "$lt" => 5 } }, where) + end + + def test_nests_correctly_alongside_an_unrelated_and_condition + where = compiled_where(NotBetweenPerson.where(:name => "Bob").where_not_between(:age, 5..25)) + assert_equal "Bob", where["name"] + assert_equal [{ "age" => { "$lt" => 5 } }, { "age" => { "$gt" => 25 } }], where["$or"] + end + + def test_raises_on_fully_open_range + assert_raises(ArgumentError) do + NotBetweenPerson.query.where_not_between(:age, nil..nil) + end + end + + def test_raises_on_invalid_value_type + assert_raises(ArgumentError) do + NotBetweenPerson.query.where_not_between(:age, 25) + end + end + + def test_raises_on_array_with_wrong_length + assert_raises(ArgumentError) do + NotBetweenPerson.query.where_not_between(:age, [5]) + end + end + + def test_raises_when_query_already_has_an_or_group_from_or_where + query = NotBetweenPerson.where(:name.eq => "a").or_where(:name.eq => "b") + assert_raises(ArgumentError) do + query.where_not_between(:age, 5..25) + end + end + + def test_raises_when_query_already_has_an_or_group_from_pipe_operator + query = NotBetweenPerson.where(:name.eq => "a") | NotBetweenPerson.where(:name.eq => "b") + assert_raises(ArgumentError) do + query.where_not_between(:age, 5..25) + end + end + + def test_raises_on_second_where_not_between_call + query = NotBetweenPerson.query.where_not_between(:age, 5..25) + assert_raises(ArgumentError) do + query.where_not_between(:name, "a".."m") + end + end + + def test_one_sided_form_does_not_trip_the_existing_or_group_guard + # Beginless/endless negation is a single condition, no new $or is + # introduced, so it's safe even when the query already has one. + query = NotBetweenPerson.where(:name.eq => "a").or_where(:name.eq => "b") + query.where_not_between(:age, 5..) + where = compiled_where(query) + assert where["$or"].present?, "the pre-existing $or group must survive" + assert_equal({ "$lt" => 5 }, where["age"]) + end +end diff --git a/test/lib/parse/query_aggregate_integration_test.rb b/test/lib/parse/query_aggregate_integration_test.rb index f7ba426..7b2af8f 100644 --- a/test/lib/parse/query_aggregate_integration_test.rb +++ b/test/lib/parse/query_aggregate_integration_test.rb @@ -1120,7 +1120,7 @@ def test_aggregate_arrays_of_pointers_and_dates # Date fields should be either Date objects or ISO strings oldest = result["oldestJoinDate"] - newest = result["newestJoinDate"] # codeql[rb/useless-assignment-to-local] + newest = result["newestJoinDate"] if oldest.is_a?(String) # Verify ISO date format @@ -1131,6 +1131,16 @@ def test_aggregate_arrays_of_pointers_and_dates assert_equal "Date", oldest["__type"], "Date should have __type: Date" assert oldest.key?("iso"), "Date should have iso field" end + + if newest.is_a?(String) + # Verify ISO date format + assert newest.match?(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/), + "Date should be in ISO format: #{newest}" + elsif newest.is_a?(Hash) && newest.key?("__type") + # Parse Date object format + assert_equal "Date", newest["__type"], "Date should have __type: Date" + assert newest.key?("iso"), "Date should have iso field" + end end end rescue => e diff --git a/test/lib/parse/query_compile_snapshot_test.rb b/test/lib/parse/query_compile_snapshot_test.rb index 2645053..e531b10 100644 --- a/test/lib/parse/query_compile_snapshot_test.rb +++ b/test/lib/parse/query_compile_snapshot_test.rb @@ -162,6 +162,38 @@ def test_between_exclusive_range_constraint assert_snapshot(compile(q), name: "between_range_exclusive", group: GROUP) end + def test_where_not_between_inclusive_range + # A fully-bounded inclusive Range compiles to $or of the two flipped, + # strict comparisons. + q = SnapPost.query.where_not_between(:likes, 10..100) + assert_snapshot(compile(q), name: "where_not_between_inclusive", group: GROUP) + end + + def test_where_not_between_exclusive_range + # An exclusive Range (`...`) flips its upper side to $gte instead of $gt. + q = SnapPost.query.where_not_between(:likes, 10...100) + assert_snapshot(compile(q), name: "where_not_between_exclusive", group: GROUP) + end + + def test_where_not_between_beginless_range + # Beginless range: `between` would only constrain the upper side, so + # its negation is a single one-sided $gt with no $or wrapper. + q = SnapPost.query.where_not_between(:likes, ..100) + assert_snapshot(compile(q), name: "where_not_between_beginless", group: GROUP) + end + + def test_where_not_between_endless_range + q = SnapPost.query.where_not_between(:likes, 10..) + assert_snapshot(compile(q), name: "where_not_between_endless", group: GROUP) + end + + def test_where_not_between_nests_inside_existing_and_condition + # The $or group must nest alongside an unrelated AND'd condition, not + # sweep it into the OR the way #or_where/`|` would. + q = SnapPost.where(:published => true).where_not_between(:likes, 10..100) + assert_snapshot(compile(q), name: "where_not_between_with_and_condition", group: GROUP) + end + def test_tags_all_array_constraint # `:tags.all => [...]` compiles to the `$all` REST operator (not a # pipeline). Snapshot under normalized array order — $all is set-semantic. diff --git a/test/lib/parse/query_integration_fast_integration_test.rb b/test/lib/parse/query_integration_fast_integration_test.rb index 4d1a9bd..446b3c9 100644 --- a/test/lib/parse/query_integration_fast_integration_test.rb +++ b/test/lib/parse/query_integration_fast_integration_test.rb @@ -52,7 +52,7 @@ def test_simple_query_operations # Test first with_timeout(2, "first query") do - first_result = GameScore.query.first # codeql[rb/useless-assignment-to-local] + GameScore.query.first # first might be nil if no data, that's ok assert true, "First query completed" end @@ -89,7 +89,7 @@ def test_class_level_methods # Test class-level first with_timeout(2, "class first") do - first = GameScore.first # codeql[rb/useless-assignment-to-local] + GameScore.first # might be nil, that's ok assert true, "Class first completed" end diff --git a/test/lib/parse/query_integration_test.rb b/test/lib/parse/query_integration_test.rb index d092ab4..8171a41 100644 --- a/test/lib/parse/query_integration_test.rb +++ b/test/lib/parse/query_integration_test.rb @@ -127,7 +127,7 @@ def test_simple_query_without_setup with_timeout(2, "simple query") do # Just try to query existing data without creating new data query = GameScore.query.limit(1) - results = query.results # codeql[rb/useless-assignment-to-local] + query.results # Don't assert anything about results - just verify query doesn't hang assert true, "Query completed without timeout" end @@ -1546,8 +1546,6 @@ def test_date_and_time_queries # Create posts with different timestamps now = Time.now - yesterday = now - 24 * 60 * 60 # codeql[rb/useless-assignment-to-local] - last_week = now - 7 * 24 * 60 * 60 # codeql[rb/useless-assignment-to-local] # Note: Parse Server automatically manages createdAt/updatedAt post1 = Post.new(title: "Recent Post", content: "New content") @@ -1583,9 +1581,6 @@ def test_mixed_where_conditions_with_dates # Create test data with various attributes now = Time.now - hour_ago = now - 3600 # codeql[rb/useless-assignment-to-local] - day_ago = now - 86400 # codeql[rb/useless-assignment-to-local] - week_ago = now - 604800 # codeql[rb/useless-assignment-to-local] # Create players with different attributes and join dates players = [] diff --git a/test/lib/parse/query_or_and_test.rb b/test/lib/parse/query_or_and_test.rb index 861d687..6377a9b 100644 --- a/test/lib/parse/query_or_and_test.rb +++ b/test/lib/parse/query_or_and_test.rb @@ -236,7 +236,7 @@ def test_table_validation # This should raise an error if we had another model begin - or_query = Parse::Query.or(product_query) # codeql[rb/useless-assignment-to-local] + Parse::Query.or(product_query) puts "Single table OR succeeded" rescue ArgumentError => e puts "Single table OR failed: #{e.message}" diff --git a/test/lib/parse/query_pointers_contains_integration_test.rb b/test/lib/parse/query_pointers_contains_integration_test.rb index 7f41bde..13014cb 100644 --- a/test/lib/parse/query_pointers_contains_integration_test.rb +++ b/test/lib/parse/query_pointers_contains_integration_test.rb @@ -537,7 +537,6 @@ def test_edge_cases_and_error_handling # Test 6: Contains with non-existent pointer puts "--- Test 6: Contains with non-existent pointer ---" fake_author_pointer = Parse::Pointer.new("QueryTestAuthor", "fakeid456") - fake_book_pointer = Parse::Pointer.new("QueryTestBook", "fakebookid789") # codeql[rb/useless-assignment-to-local] library = QueryTestLibrary.new( name: "Edge Case Library", diff --git a/test/lib/parse/security_hardening_test.rb b/test/lib/parse/security_hardening_test.rb index 809f724..0174758 100644 --- a/test/lib/parse/security_hardening_test.rb +++ b/test/lib/parse/security_hardening_test.rb @@ -508,7 +508,7 @@ class SHAliasOwner < Parse::Object def test_array_parse_objects_ignores_hash_className_when_caller_specifies arr = [{ "__type" => "Pointer", "className" => "_Session", "objectId" => "evil" }] - out, _err = capture_io { arr.parse_objects("Author") } # codeql[rb/useless-assignment-to-local] + _, _err = capture_io { arr.parse_objects("Author") } objs = arr.parse_objects("Author") assert_equal 1, objs.length assert_equal "Author", objs.first.parse_class diff --git a/test/lib/parse/time_query_integration_test.rb b/test/lib/parse/time_query_integration_test.rb index 801a8e3..28a2774 100644 --- a/test/lib/parse/time_query_integration_test.rb +++ b/test/lib/parse/time_query_integration_test.rb @@ -319,8 +319,6 @@ def test_utc_timezone_handling # Create times in different timezone formats utc_time = Time.now.utc local_time = Time.now - datetime_utc = DateTime.now.utc # codeql[rb/useless-assignment-to-local] - datetime_local = DateTime.now # codeql[rb/useless-assignment-to-local] # Create event with UTC time utc_event = Event.new({ diff --git a/test/lib/parse/upsert_methods_integration_test.rb b/test/lib/parse/upsert_methods_integration_test.rb index 11d68e3..50e39d2 100644 --- a/test/lib/parse/upsert_methods_integration_test.rb +++ b/test/lib/parse/upsert_methods_integration_test.rb @@ -323,7 +323,7 @@ def test_performance_comparison_across_methods # Test create_or_update! performance (with changes) start_time = Time.now 5.times do |i| - result = UpsertTestUser.create_or_update!({ email: "perf@example.com" }, { age: 35 + i }) # codeql[rb/useless-assignment-to-local] + UpsertTestUser.create_or_update!({ email: "perf@example.com" }, { age: 35 + i }) end create_or_update_with_change_time = Time.now - start_time diff --git a/test/lib/parse/webhook_triggers_test.rb b/test/lib/parse/webhook_triggers_test.rb index 9620889..a0679c2 100644 --- a/test/lib/parse/webhook_triggers_test.rb +++ b/test/lib/parse/webhook_triggers_test.rb @@ -78,7 +78,7 @@ def test_before_save_trigger } client_payload = Parse::Webhooks::Payload.new(client_payload_data) - result = Parse::Webhooks.call_route(:before_save, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] + Parse::Webhooks.call_route(:before_save, "TestObject", client_payload) assert hook_called, "before_save hook should be called for client" assert hook_payload.before_save?, "Payload should identify as before_save" @@ -153,7 +153,7 @@ def test_after_save_trigger # Add after_create callback for new objects test_object.define_singleton_method(:run_after_create_callbacks) { callback_executed = true } - result = Parse::Webhooks.call_route(:after_save, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] + Parse::Webhooks.call_route(:after_save, "TestObject", client_payload) Parse::Webhooks.run_after_save_chain(client_payload) assert hook_called, "after_save hook should be called for client" @@ -222,7 +222,7 @@ def test_before_delete_trigger client_payload = Parse::Webhooks::Payload.new(client_payload_data) client_payload.define_singleton_method(:parse_object) { test_object } - result = Parse::Webhooks.call_route(:before_delete, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] + Parse::Webhooks.call_route(:before_delete, "TestObject", client_payload) assert hook_called, "before_delete hook should be called for client" assert hook_payload.before_delete?, "Payload should identify as before_delete" @@ -282,7 +282,7 @@ def test_after_delete_trigger } client_payload = Parse::Webhooks::Payload.new(client_payload_data) - result = Parse::Webhooks.call_route(:after_delete, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] + Parse::Webhooks.call_route(:after_delete, "TestObject", client_payload) assert hook_called, "after_delete hook should be called for client" assert hook_payload.after_delete?, "Payload should identify as after_delete" @@ -347,7 +347,7 @@ def test_before_find_trigger client_payload = Parse::Webhooks::Payload.new(client_payload_data) client_payload.instance_variable_set(:@webhook_class, "TestObject") - result = Parse::Webhooks.call_route(:before_find, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] + Parse::Webhooks.call_route(:before_find, "TestObject", client_payload) assert hook_called, "before_find hook should be called for client" assert hook_payload.before_find?, "Payload should identify as before_find" @@ -421,7 +421,7 @@ def test_after_find_trigger client_payload = Parse::Webhooks::Payload.new(client_payload_data) client_payload.instance_variable_set(:@webhook_class, "TestObject") - result = Parse::Webhooks.call_route(:after_find, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] + Parse::Webhooks.call_route(:after_find, "TestObject", client_payload) assert hook_called, "after_find hook should be called for client" assert hook_payload.after_find?, "Payload should identify as after_find" @@ -534,7 +534,7 @@ def test_multiple_trigger_hooks } before_payload = Parse::Webhooks::Payload.new(before_payload_data) - result = Parse::Webhooks.call_route(:before_save, "TestObject", before_payload) # codeql[rb/useless-assignment-to-local] + Parse::Webhooks.call_route(:before_save, "TestObject", before_payload) assert_equal ["before2"], execution_order, "Only the last before_save hook should execute" puts "✅ Single before_save hook behavior works correctly" @@ -580,7 +580,7 @@ def test_trigger_error_handling # Direct call_route won't raise the error, but the error! method would be called # This tests that the conditional logic works correctly begin - result = Parse::Webhooks.call_route(:before_save, "TestObject", client_payload) # codeql[rb/useless-assignment-to-local] + Parse::Webhooks.call_route(:before_save, "TestObject", client_payload) flunk "Should have raised ResponseError for client request" rescue Parse::Webhooks::ResponseError => e assert_equal "Client validation failed", e.message, "Should have correct error message" @@ -616,7 +616,7 @@ def test_wildcard_trigger_routing payload = Parse::Webhooks::Payload.new(payload_data) payload.define_singleton_method(:parse_object) { nil } - result = Parse::Webhooks.call_route(:after_save, "TestObject", payload) # codeql[rb/useless-assignment-to-local] + Parse::Webhooks.call_route(:after_save, "TestObject", payload) assert specific_called, "Specific hook should be called" refute wildcard_called, "Wildcard hook should not be called when specific exists" @@ -639,7 +639,7 @@ def test_wildcard_trigger_routing assert_nil result, "No specific route should exist" # Then try wildcard route - result = Parse::Webhooks.call_route(:after_save, "*", unknown_payload) # codeql[rb/useless-assignment-to-local] + Parse::Webhooks.call_route(:after_save, "*", unknown_payload) refute specific_called, "Specific hook should not be called" assert wildcard_called, "Wildcard hook should be called for unknown class" diff --git a/test/snapshots/query_compile/where_not_between_beginless.json b/test/snapshots/query_compile/where_not_between_beginless.json new file mode 100644 index 0000000..b4862ae --- /dev/null +++ b/test/snapshots/query_compile/where_not_between_beginless.json @@ -0,0 +1,7 @@ +{ + "where": { + "likes": { + "$gt": 100 + } + } +} diff --git a/test/snapshots/query_compile/where_not_between_endless.json b/test/snapshots/query_compile/where_not_between_endless.json new file mode 100644 index 0000000..76b5655 --- /dev/null +++ b/test/snapshots/query_compile/where_not_between_endless.json @@ -0,0 +1,7 @@ +{ + "where": { + "likes": { + "$lt": 10 + } + } +} diff --git a/test/snapshots/query_compile/where_not_between_exclusive.json b/test/snapshots/query_compile/where_not_between_exclusive.json new file mode 100644 index 0000000..031ebcc --- /dev/null +++ b/test/snapshots/query_compile/where_not_between_exclusive.json @@ -0,0 +1,16 @@ +{ + "where": { + "$or": [ + { + "likes": { + "$lt": 10 + } + }, + { + "likes": { + "$gte": 100 + } + } + ] + } +} diff --git a/test/snapshots/query_compile/where_not_between_inclusive.json b/test/snapshots/query_compile/where_not_between_inclusive.json new file mode 100644 index 0000000..5a5a05c --- /dev/null +++ b/test/snapshots/query_compile/where_not_between_inclusive.json @@ -0,0 +1,16 @@ +{ + "where": { + "$or": [ + { + "likes": { + "$lt": 10 + } + }, + { + "likes": { + "$gt": 100 + } + } + ] + } +} diff --git a/test/snapshots/query_compile/where_not_between_with_and_condition.json b/test/snapshots/query_compile/where_not_between_with_and_condition.json new file mode 100644 index 0000000..665176a --- /dev/null +++ b/test/snapshots/query_compile/where_not_between_with_and_condition.json @@ -0,0 +1,17 @@ +{ + "where": { + "$or": [ + { + "likes": { + "$lt": 10 + } + }, + { + "likes": { + "$gt": 100 + } + } + ], + "published": true + } +} diff --git a/test/support/docker_helper.rb b/test/support/docker_helper.rb index 4e3ae24..74c0a39 100644 --- a/test/support/docker_helper.rb +++ b/test/support/docker_helper.rb @@ -16,7 +16,7 @@ def start! puts "Starting Parse Server test container..." - stdout, stderr, status = Open3.capture3("docker-compose -f #{COMPOSE_FILE} up -d") # codeql[rb/useless-assignment-to-local] + _stdout, stderr, status = Open3.capture3("docker-compose -f #{COMPOSE_FILE} up -d") if status.success? wait_for_server diff --git a/test/support/test_server.rb b/test/support/test_server.rb index e7cd13c..5faa993 100644 --- a/test/support/test_server.rb +++ b/test/support/test_server.rb @@ -37,14 +37,14 @@ def server_available? uri = URI(Parse::Client.client.server_url + "/health") response = Net::HTTP.get_response(uri) response.code == "200" - rescue StandardError => e # codeql[rb/useless-assignment-to-local] + rescue StandardError # Fallback: Try to check if Parse is responding at all begin uri = URI(Parse::Client.client.server_url) response = Net::HTTP.get_response(uri) # Parse Server typically returns 404 or 401 for root path but it means server is up ["200", "404", "401", "403"].include?(response.code) - rescue StandardError => e2 # codeql[rb/useless-assignment-to-local] + rescue StandardError false end end @@ -82,12 +82,12 @@ def reset_database! begin obj.destroy total_deleted += 1 - rescue => e # codeql[rb/useless-assignment-to-local] + rescue # Silent failure - continue with other objects end end end - rescue StandardError => e # codeql[rb/useless-assignment-to-local] + rescue StandardError # Silent failure - continue with other classes end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 3d9f855..9a608b1 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -57,7 +57,7 @@ def test_scalar_values module Minitest module Assertions def refute_raises(*exp) - msg = "#{exp.pop}.\n" if String === exp.last # codeql[rb/useless-assignment-to-local] + "#{exp.pop}.\n" if String === exp.last begin yield @@ -65,7 +65,6 @@ def refute_raises(*exp) return e if exp.include? Minitest::Skip raise e rescue Exception => e - exp = exp.first if exp.size == 1 # codeql[rb/useless-assignment-to-local] flunk "unexpected exception raised: #{e}" end end