diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 7cffe01a99220f5514ce2426b0e4e7bb362a0962..2e9c3f9439d34c9677fc0f7feca1488fc068ca8f 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -16,7 +16,7 @@ jobs:
       - name: Check out code
         uses: actions/checkout@v2
       - name: Set up Ruby
-        uses: actions/setup-ruby@v1
+        uses: ruby/setup-ruby@v1
         with:
           ruby-version: "2.4"
       - name: Set up Gems
@@ -34,8 +34,8 @@ jobs:
       fail-fast: false
       matrix:
         os: ["ubuntu-latest"]
-        redis: ["6.0"]
-        ruby: ["2.7", "2.6", "2.5", "2.4"]
+        redis: ["6.2"]
+        ruby: ["3.0", "2.7", "2.6", "2.5"]
         driver: ["ruby", "hiredis", "synchrony"]
     runs-on: ${{ matrix.os }}
     env:
@@ -54,7 +54,7 @@ jobs:
           echo "DRIVER=${DRIVER}"
           echo "REDIS_BRANCH=${REDIS_BRANCH}"
       - name: Set up Ruby
-        uses: actions/setup-ruby@v1
+        uses: ruby/setup-ruby@v1
         with:
           ruby-version: ${{ matrix.ruby }}
       - name: Cache dependent gems
@@ -87,8 +87,8 @@ jobs:
       fail-fast: false
       matrix:
         os: ["ubuntu-latest"]
-        redis: ["5.0", "4.0", "3.2", "3.0"]
-        ruby: ["jruby-9.2.9.0", "2.3"]
+        redis: ["6.0", "5.0", "4.0", "3.2", "3.0"]
+        ruby: ["jruby-9.2.9.0", "2.4"]
         driver: ["ruby"]
     runs-on: ${{ matrix.os }}
     env:
diff --git a/.rubocop.yml b/.rubocop.yml
index 96fb535ce441d84831c315a23f22c83c61404812..7942ca64b6fa53e86f3f4b6b10808d62952f2ca7 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,5 +1,5 @@
 AllCops:
-  TargetRubyVersion: 2.3
+  TargetRubyVersion: 2.4
 
 Layout/LineLength:
   Max: 120
@@ -100,12 +100,15 @@ Style/WordArray:
 Lint/NonLocalExitFromIterator:
   Enabled: false
 
-Lint/EndAlignment:
+Layout/EndAlignment:
   EnforcedStyleAlignWith: variable
 
 Layout/ElseAlignment:
   Enabled: false
 
+Layout/RescueEnsureAlignment:
+  Enabled: false
+
 Naming/HeredocDelimiterNaming:
   Enabled: false
 
@@ -121,3 +124,6 @@ Naming/AccessorMethodName:
 
 Naming/MethodParameterName:
   Enabled: false
+
+Metrics/BlockNesting:
+  Enabled: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5973df5ed8460cf334482782c3025d018c00c6b3..b83ac83c5ea0da7c838fb5277a6ede3d526e9ff3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,43 @@
 # Unreleased
 
+# 4.5.1
+
+* Restore the accidential auth behavior of redis-rb 4.3.0 with a warning. If provided with the `default` user's password, but a wrong username,
+  redis-rb will first try to connect as the provided user, but then will fallback to connect as the `default` user with the provided password.
+  This behavior is deprecated and will be removed in Redis 4.6.0. Fix #1038.
+
+# 4.5.0
+
+* Handle parts of the command using incompatible encodings. See #1037.
+* Add GET option to SET command. See #1036.
+* Add ZRANDMEMBER command. See #1035.
+* Add LMOVE/BLMOVE commands. See #1034.
+* Add ZMSCORE command. See #1032.
+* Add LT/GT options to ZADD. See #1033.
+* Add SMISMEMBER command. See #1031.
+* Add EXAT/PXAT options to SET. See #1028.
+* Add GETDEL/GETEX commands. See #1024.
+* `Redis#exists` now returns an Integer by default, as warned since 4.2.0. The old behavior can be restored with `Redis.exists_returns_integer = false`.
+* Fix Redis < 6 detection during connect. See #1025.
+* Fix fetching command details in Redis cluster when the first node is unhealthy. See #1026.
+
+# 4.4.0
+
+* Redis cluster: fix cross-slot validation in pipelines. Fix ##1019.
+* Add support for `XAUTOCLAIM`. See #1018.
+* Properly issue `READONLY` when reconnecting to replicas. Fix #1017.
+* Make `del` a noop if passed an empty list of keys. See #998.
+* Add support for `ZINTER`. See #995.
+
+# 4.3.1
+
+* Fix password authentication against redis server 5 and older.
+
+# 4.3.0
+
+* Add the TYPE argument to scan and scan_each. See #985.
+* Support AUTH command for ACL. See #967.
+
 # 4.2.5
 
 * Optimize the ruby connector write buffering. See #964.
diff --git a/README.md b/README.md
index f313033d53e570fd97278f2ea626b03fffdc2f1c..b8591ca50390deff470a21f3faeda92a1f3b5a54 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# redis-rb [![Build Status][travis-image]][travis-link] [![Inline docs][inchpages-image]][inchpages-link] ![](https://github.com/redis/redis-rb/workflows/Test/badge.svg?branch=master)
+# redis-rb [![Build Status][gh-actions-image]][gh-actions-link] [![Inline docs][inchpages-image]][inchpages-link]
 
 A Ruby client that tries to match [Redis][redis-home]' API one-to-one, while still
 providing an idiomatic interface.
@@ -54,6 +54,12 @@ To connect to a password protected Redis instance, use:
 redis = Redis.new(password: "mysecret")
 ```
 
+To connect a Redis instance using [ACL](https://redis.io/topics/acl), use:
+
+```ruby
+redis = Redis.new(username: 'myname', password: 'mysecret')
+```
+
 The Redis class exports methods that are named identical to the commands
 they execute. The arguments these methods accept are often identical to
 the arguments specified on the [Redis website][redis-commands]. For
@@ -440,7 +446,7 @@ redis = Redis.new(:driver => :synchrony)
 ## Testing
 
 This library is tested against recent Ruby and Redis versions.
-Check [Travis][travis-link] for the exact versions supported.
+Check [Github Actions][gh-actions-link] for the exact versions supported.
 
 ## See Also
 
@@ -459,12 +465,11 @@ client and evangelized Redis in Rubyland. Thank you, Ezra.
 requests.
 
 
-[inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
-[inchpages-link]:  https://inch-ci.org/github/redis/redis-rb
-[redis-commands]:  https://redis.io/commands
-[redis-home]:      https://redis.io
-[redis-url]:       http://www.iana.org/assignments/uri-schemes/prov/redis
-[travis-home]:     https://travis-ci.org/
-[travis-image]:    https://secure.travis-ci.org/redis/redis-rb.svg?branch=master
-[travis-link]:     https://travis-ci.org/redis/redis-rb
-[rubydoc]:         http://www.rubydoc.info/gems/redis
+[inchpages-image]:  https://inch-ci.org/github/redis/redis-rb.svg
+[inchpages-link]:   https://inch-ci.org/github/redis/redis-rb
+[redis-commands]:   https://redis.io/commands
+[redis-home]:       https://redis.io
+[redis-url]:        http://www.iana.org/assignments/uri-schemes/prov/redis
+[gh-actions-image]: https://github.com/redis/redis-rb/workflows/Test/badge.svg
+[gh-actions-link]:  https://github.com/redis/redis-rb/actions
+[rubydoc]:          http://www.rubydoc.info/gems/redis
diff --git a/Rakefile b/Rakefile
index 268b1568d6375961a355aab4cf28efd3245ab268..fa56d55c47964c30c0e94ee595f9113932a1323b 100644
--- a/Rakefile
+++ b/Rakefile
@@ -2,18 +2,8 @@
 
 require 'bundler/gem_tasks'
 require 'rake/testtask'
-Rake::TestTask.new :test do |t|
-  if ENV['SOCKET_PATH'].nil?
-    sock_file = Dir.glob("#{__dir__}/**/redis.sock").first
-
-    if sock_file.nil?
-      puts '`SOCKET_PATH` environment variable required'
-      exit 1
-    end
-
-    ENV['SOCKET_PATH'] = sock_file
-  end
 
+Rake::TestTask.new :test do |t|
   t.libs = %w(lib test)
 
   if ARGV.size == 1
@@ -25,4 +15,21 @@ Rake::TestTask.new :test do |t|
   t.options = '-v' if ENV['CI'] || ENV['VERBOSE']
 end
 
+namespace :test do
+  task :set_socket_path do
+    if ENV['SOCKET_PATH'].nil?
+      sock_file = Dir.glob("#{__dir__}/**/redis.sock").first
+
+      if sock_file.nil?
+        puts '`SOCKET_PATH` environment variable required'
+        exit 1
+      end
+
+      ENV['SOCKET_PATH'] = sock_file
+    end
+  end
+end
+
+Rake::Task[:test].enhance(["test:set_socket_path"])
+
 task default: :test
diff --git a/benchmarking/speed.rb b/benchmarking/speed.rb
index f8ab1587972934dc4840145b9a1fbb116f30592f..cb5bc46a74fff5791ff2bfb2bbf4ec3872829795 100644
--- a/benchmarking/speed.rb
+++ b/benchmarking/speed.rb
@@ -1,9 +1,6 @@
 # frozen_string_literal: true
 
-# Run with
-#
-#   $ ruby -Ilib benchmarking/speed.rb
-#
+$LOAD_PATH.push File.join(__dir__, 'lib')
 
 require "benchmark"
 require "redis"
@@ -15,8 +12,8 @@ elapsed = Benchmark.realtime do
   # n sets, n gets
   n.times do |i|
     key = "foo#{i}"
-    r[key] = key * 10
-    r[key]
+    r.set(key, key * 10)
+    r.get(key)
   end
 end
 
diff --git a/bin/build b/bin/build
index 7f2c788ab8da4624caada53135d2fb7caf69b23c..925a91059a68473bcc30a88c53d8db0e05923d5c 100755
--- a/bin/build
+++ b/bin/build
@@ -4,6 +4,7 @@
 TARBALL = ARGV[0]
 
 require 'digest/sha1'
+require 'English'
 require 'fileutils'
 
 class Builder
diff --git a/lib/redis.rb b/lib/redis.rb
index 8410ac8b6807dbd89d691a0a9ec4828f588d89cd..5ae2e387a088842839798632c2357f325e585ac6 100644
--- a/lib/redis.rb
+++ b/lib/redis.rb
@@ -4,6 +4,8 @@ require "monitor"
 require_relative "redis/errors"
 
 class Redis
+  @exists_returns_integer = true
+
   class << self
     attr_reader :exists_returns_integer
 
@@ -39,6 +41,7 @@ class Redis
   # @option options [String] :path path to server socket (overrides host and port)
   # @option options [Float] :timeout (5.0) timeout in seconds
   # @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds
+  # @option options [String] :username Username to authenticate against server
   # @option options [String] :password Password to authenticate against server
   # @option options [Integer] :db (0) Database to select after initial connect
   # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`, `:synchrony`
@@ -143,12 +146,13 @@ class Redis
 
   # Authenticate to the server.
   #
-  # @param [String] password must match the password specified in the
-  #   `requirepass` directive in the configuration file
+  # @param [Array<String>] args includes both username and password
+  #   or only password
   # @return [String] `OK`
-  def auth(password)
+  # @see https://redis.io/commands/auth AUTH command
+  def auth(*args)
     synchronize do |client|
-      client.call([:auth, password])
+      client.call([:auth, *args])
     end
   end
 
@@ -553,6 +557,9 @@ class Redis
   # @param [String, Array<String>] keys
   # @return [Integer] number of keys that were deleted
   def del(*keys)
+    keys.flatten!(1)
+    return 0 if keys.empty?
+
     synchronize do |client|
       client.call([:del] + keys)
     end
@@ -829,17 +836,23 @@ class Redis
   # @param [Hash] options
   #   - `:ex => Integer`: Set the specified expire time, in seconds.
   #   - `:px => Integer`: Set the specified expire time, in milliseconds.
+  #   - `:exat => Integer` : Set the specified Unix time at which the key will expire, in seconds.
+  #   - `:pxat => Integer` : Set the specified Unix time at which the key will expire, in milliseconds.
   #   - `:nx => true`: Only set the key if it does not already exist.
   #   - `:xx => true`: Only set the key if it already exist.
   #   - `:keepttl => true`: Retain the time to live associated with the key.
+  #   - `:get => true`: Return the old string stored at key, or nil if key did not exist.
   # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
-  def set(key, value, ex: nil, px: nil, nx: nil, xx: nil, keepttl: nil)
+  def set(key, value, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil, keepttl: nil, get: nil)
     args = [:set, key, value.to_s]
     args << "EX" << ex if ex
     args << "PX" << px if px
+    args << "EXAT" << exat if exat
+    args << "PXAT" << pxat if pxat
     args << "NX" if nx
     args << "XX" if xx
     args << "KEEPTTL" if keepttl
+    args << "GET" if get
 
     synchronize do |client|
       if nx || xx
@@ -1105,6 +1118,45 @@ class Redis
     end
   end
 
+  # Get the value of key and delete the key. This command is similar to GET,
+  # except for the fact that it also deletes the key on success.
+  #
+  # @param [String] key
+  # @return [String] the old value stored in the key, or `nil` if the key
+  #   did not exist
+  def getdel(key)
+    synchronize do |client|
+      client.call([:getdel, key])
+    end
+  end
+
+  # Get the value of key and optionally set its expiration. GETEX is similar to
+  # GET, but is a write command with additional options. When no options are
+  # provided, GETEX behaves like GET.
+  #
+  # @param [String] key
+  # @param [Hash] options
+  #   - `:ex => Integer`: Set the specified expire time, in seconds.
+  #   - `:px => Integer`: Set the specified expire time, in milliseconds.
+  #   - `:exat => true`: Set the specified Unix time at which the key will
+  #      expire, in seconds.
+  #   - `:pxat => true`: Set the specified Unix time at which the key will
+  #      expire, in milliseconds.
+  #   - `:persist => true`: Remove the time to live associated with the key.
+  # @return [String] The value of key, or nil when key does not exist.
+  def getex(key, ex: nil, px: nil, exat: nil, pxat: nil, persist: false)
+    args = [:getex, key]
+    args << "EX" << ex if ex
+    args << "PX" << px if px
+    args << "EXAT" << exat if exat
+    args << "PXAT" << pxat if pxat
+    args << "PERSIST" if persist
+
+    synchronize do |client|
+      client.call(args)
+    end
+  end
+
   # Get the length of the value stored in a key.
   #
   # @param [String] key
@@ -1126,6 +1178,59 @@ class Redis
     end
   end
 
+  # Remove the first/last element in a list, append/prepend it to another list and return it.
+  #
+  # @param [String] source source key
+  # @param [String] destination destination key
+  # @param [String, Symbol] where_source from where to remove the element from the source list
+  #     e.g. 'LEFT' - from head, 'RIGHT' - from tail
+  # @param [String, Symbol] where_destination where to push the element to the source list
+  #     e.g. 'LEFT' - to head, 'RIGHT' - to tail
+  #
+  # @return [nil, String] the element, or nil when the source key does not exist
+  #
+  # @note This command comes in place of the now deprecated RPOPLPUSH.
+  #     Doing LMOVE RIGHT LEFT is equivalent.
+  def lmove(source, destination, where_source, where_destination)
+    where_source, where_destination = _normalize_move_wheres(where_source, where_destination)
+
+    synchronize do |client|
+      client.call([:lmove, source, destination, where_source, where_destination])
+    end
+  end
+
+  # Remove the first/last element in a list and append/prepend it
+  # to another list and return it, or block until one is available.
+  #
+  # @example With timeout
+  #   element = redis.blmove("foo", "bar", "LEFT", "RIGHT", timeout: 5)
+  #     # => nil on timeout
+  #     # => "element" on success
+  # @example Without timeout
+  #   element = redis.blmove("foo", "bar", "LEFT", "RIGHT")
+  #     # => "element"
+  #
+  # @param [String] source source key
+  # @param [String] destination destination key
+  # @param [String, Symbol] where_source from where to remove the element from the source list
+  #     e.g. 'LEFT' - from head, 'RIGHT' - from tail
+  # @param [String, Symbol] where_destination where to push the element to the source list
+  #     e.g. 'LEFT' - to head, 'RIGHT' - to tail
+  # @param [Hash] options
+  #   - `:timeout => Numeric`: timeout in seconds, defaults to no timeout
+  #
+  # @return [nil, String] the element, or nil when the source key does not exist or the timeout expired
+  #
+  def blmove(source, destination, where_source, where_destination, timeout: 0)
+    where_source, where_destination = _normalize_move_wheres(where_source, where_destination)
+
+    synchronize do |client|
+      command = [:blmove, source, destination, where_source, where_destination, timeout]
+      timeout += client.timeout if timeout > 0
+      client.call_with_timeout(command, timeout)
+    end
+  end
+
   # Prepend one or more values to a list, creating the list if it doesn't exist
   #
   # @param [String] key
@@ -1170,23 +1275,29 @@ class Redis
     end
   end
 
-  # Remove and get the first element in a list.
+  # Remove and get the first elements in a list.
   #
   # @param [String] key
-  # @return [String]
-  def lpop(key)
+  # @param [Integer] count number of elements to remove
+  # @return [String, Array<String>] the values of the first elements
+  def lpop(key, count = nil)
     synchronize do |client|
-      client.call([:lpop, key])
+      command = [:lpop, key]
+      command << count if count
+      client.call(command)
     end
   end
 
-  # Remove and get the last element in a list.
+  # Remove and get the last elements in a list.
   #
   # @param [String] key
-  # @return [String]
-  def rpop(key)
+  # @param [Integer] count number of elements to remove
+  # @return [String, Array<String>] the values of the last elements
+  def rpop(key, count = nil)
     synchronize do |client|
-      client.call([:rpop, key])
+      command = [:rpop, key]
+      command << count if count
+      client.call(command)
     end
   end
 
@@ -1468,6 +1579,19 @@ class Redis
     end
   end
 
+  # Determine if multiple values are members of a set.
+  #
+  # @param [String] key
+  # @param [String, Array<String>] members
+  # @return [Array<Boolean>]
+  def smismember(key, *members)
+    synchronize do |client|
+      client.call([:smismember, key, *members]) do |reply|
+        reply.map(&Boolify)
+      end
+    end
+  end
+
   # Get all the members in a set.
   #
   # @param [String] key
@@ -1572,6 +1696,10 @@ class Redis
   #   add elements)
   #   - `:nx => true`: Don't update already existing elements (always
   #   add new elements)
+  #   - `:lt => true`: Only update existing elements if the new score
+  #   is less than the current score
+  #   - `:gt => true`: Only update existing elements if the new score
+  #   is greater than the current score
   #   - `:ch => true`: Modify the return value from the number of new
   #   elements added, to the total number of elements changed (CH is an
   #   abbreviation of changed); changed elements are new elements added
@@ -1586,10 +1714,12 @@ class Redis
   #   pairs that were **added** to the sorted set.
   #   - `Float` when option :incr is specified, holding the score of the member
   #   after incrementing it.
-  def zadd(key, *args, nx: nil, xx: nil, ch: nil, incr: nil)
+  def zadd(key, *args, nx: nil, xx: nil, lt: nil, gt: nil, ch: nil, incr: nil)
     command = [:zadd, key]
     command << "NX" if nx
     command << "XX" if xx
+    command << "LT" if lt
+    command << "GT" if gt
     command << "CH" if ch
     command << "INCR" if incr
 
@@ -1752,6 +1882,63 @@ class Redis
     end
   end
 
+  # Get the scores associated with the given members in a sorted set.
+  #
+  # @example Get the scores for members "a" and "b"
+  #   redis.zmscore("zset", "a", "b")
+  #     # => [32.0, 48.0]
+  #
+  # @param [String] key
+  # @param [String, Array<String>] members
+  # @return [Array<Float>] scores of the members
+  def zmscore(key, *members)
+    synchronize do |client|
+      client.call([:zmscore, key, *members]) do |reply|
+        reply.map(&Floatify)
+      end
+    end
+  end
+
+  # Get one or more random members from a sorted set.
+  #
+  # @example Get one random member
+  #   redis.zrandmember("zset")
+  #     # => "a"
+  # @example Get multiple random members
+  #   redis.zrandmember("zset", 2)
+  #     # => ["a", "b"]
+  # @example Gem multiple random members with scores
+  #   redis.zrandmember("zset", 2, with_scores: true)
+  #     # => [["a", 2.0], ["b", 3.0]]
+  #
+  # @param [String] key
+  # @param [Integer] count
+  # @param [Hash] options
+  #   - `:with_scores => true`: include scores in output
+  #
+  # @return [nil, String, Array<String>, Array<[String, Float]>]
+  #   - when `key` does not exist or set is empty, `nil`
+  #   - when `count` is not specified, a member
+  #   - when `count` is specified and `:with_scores` is not specified, an array of members
+  #   - when `:with_scores` is specified, an array with `[member, score]` pairs
+  def zrandmember(key, count = nil, withscores: false, with_scores: withscores)
+    if with_scores && count.nil?
+      raise ArgumentError, "count argument must be specified"
+    end
+
+    args = [:zrandmember, key]
+    args << count if count
+
+    if with_scores
+      args << "WITHSCORES"
+      block = FloatifyPairs
+    end
+
+    synchronize do |client|
+      client.call(args, &block)
+    end
+  end
+
   # Return a range of members in a sorted set, by index.
   #
   # @example Retrieve all members from a sorted set
@@ -2054,6 +2241,45 @@ class Redis
     end
   end
 
+  # Return the intersection of multiple sorted sets
+  #
+  # @example Retrieve the intersection of `2*zsetA` and `1*zsetB`
+  #   redis.zinter("zsetA", "zsetB", :weights => [2.0, 1.0])
+  #     # => ["v1", "v2"]
+  # @example Retrieve the intersection of `2*zsetA` and `1*zsetB`, and their scores
+  #   redis.zinter("zsetA", "zsetB", :weights => [2.0, 1.0], :with_scores => true)
+  #     # => [["v1", 3.0], ["v2", 6.0]]
+  #
+  # @param [String, Array<String>] keys one or more keys to intersect
+  # @param [Hash] options
+  #   - `:weights => [Float, Float, ...]`: weights to associate with source
+  #   sorted sets
+  #   - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
+  #   - `:with_scores => true`: include scores in output
+  #
+  # @return [Array<String>, Array<[String, Float]>]
+  #   - when `:with_scores` is not specified, an array of members
+  #   - when `:with_scores` is specified, an array with `[member, score]` pairs
+  def zinter(*keys, weights: nil, aggregate: nil, with_scores: false)
+    args = [:zinter, keys.size, *keys]
+
+    if weights
+      args << "WEIGHTS"
+      args.concat(weights)
+    end
+
+    args << "AGGREGATE" << aggregate if aggregate
+
+    if with_scores
+      args << "WITHSCORES"
+      block = FloatifyPairs
+    end
+
+    synchronize do |client|
+      client.call(args, &block)
+    end
+  end
+
   # Intersect multiple sorted sets and store the resulting sorted set in a new
   # key.
   #
@@ -2636,12 +2862,13 @@ class Redis
     _eval(:evalsha, args)
   end
 
-  def _scan(command, cursor, args, match: nil, count: nil, &block)
+  def _scan(command, cursor, args, match: nil, count: nil, type: nil, &block)
     # SSCAN/ZSCAN/HSCAN already prepend the key to +args+.
 
     args << cursor
     args << "MATCH" << match if match
     args << "COUNT" << count if count
+    args << "TYPE" << type if type
 
     synchronize do |client|
       client.call([command] + args, &block)
@@ -2656,11 +2883,15 @@ class Redis
   # @example Retrieve a batch of keys matching a pattern
   #   redis.scan(4, :match => "key:1?")
   #     # => ["92", ["key:13", "key:18"]]
+  # @example Retrieve a batch of keys of a certain type
+  #   redis.scan(92, :type => "zset")
+  #     # => ["173", ["sortedset:14", "sortedset:78"]]
   #
   # @param [String, Integer] cursor the cursor of the iteration
   # @param [Hash] options
   #   - `:match => String`: only return keys matching the pattern
   #   - `:count => Integer`: return count keys at most per iteration
+  #   - `:type => String`: return keys only of the given type
   #
   # @return [String, Array<String>] the next cursor and all found keys
   def scan(cursor, **options)
@@ -2676,10 +2907,15 @@ class Redis
   #   redis.scan_each(:match => "key:1?") {|key| puts key}
   #     # => key:13
   #     # => key:18
+  # @example Execute block for each key of a type
+  #   redis.scan_each(:type => "hash") {|key| puts redis.type(key)}
+  #     # => "hash"
+  #     # => "hash"
   #
   # @param [Hash] options
   #   - `:match => String`: only return keys matching the pattern
   #   - `:count => Integer`: return count keys at most per iteration
+  #   - `:type => String`: return keys only of the given type
   #
   # @return [Enumerator] an enumerator for all found keys
   def scan_each(**options, &block)
@@ -3220,6 +3456,38 @@ class Redis
     synchronize { |client| client.call(args, &blk) }
   end
 
+  # Transfers ownership of pending stream entries that match the specified criteria.
+  #
+  # @example Claim next pending message stuck > 5 minutes  and mark as retry
+  #   redis.xautoclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0')
+  # @example Claim 50 next pending messages stuck > 5 minutes  and mark as retry
+  #   redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0', count: 50)
+  # @example Claim next pending message stuck > 5 minutes and don't mark as retry
+  #   redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0', justid: true)
+  # @example Claim next pending message after this id stuck > 5 minutes  and mark as retry
+  #   redis.xautoclaim('mystream', 'mygroup', 'consumer1', 3600000, '1641321233-0')
+  #
+  # @param key           [String]        the stream key
+  # @param group         [String]        the consumer group name
+  # @param consumer      [String]        the consumer name
+  # @param min_idle_time [Integer]       the number of milliseconds
+  # @param start         [String]        entry id to start scanning from or 0-0 for everything
+  # @param count         [Integer]       number of messages to claim (default 1)
+  # @param justid        [Boolean]       whether to fetch just an array of entry ids or not.
+  #                                      Does not increment retry count when true
+  #
+  # @return [Hash{String => Hash}] the entries successfully claimed
+  # @return [Array<String>]        the entry ids successfully claimed if justid option is `true`
+  def xautoclaim(key, group, consumer, min_idle_time, start, count: nil, justid: false)
+    args = [:xautoclaim, key, group, consumer, min_idle_time, start]
+    if count
+      args << 'COUNT' << count.to_s
+    end
+    args << 'JUSTID' if justid
+    blk = justid ? HashifyStreamAutoclaimJustId : HashifyStreamAutoclaim
+    synchronize { |client| client.call(args, &blk) }
+  end
+
   # Fetches not acknowledging pending entries
   #
   # @example With key and group
@@ -3426,10 +3694,24 @@ class Redis
 
   HashifyStreamEntries = lambda { |reply|
     reply.compact.map do |entry_id, values|
-      [entry_id, values.each_slice(2).to_h]
+      [entry_id, values&.each_slice(2)&.to_h]
     end
   }
 
+  HashifyStreamAutoclaim = lambda { |reply|
+    {
+      'next' => reply[0],
+      'entries' => reply[1].map { |entry| [entry[0], entry[1].each_slice(2).to_h] }
+    }
+  }
+
+  HashifyStreamAutoclaimJustId = lambda { |reply|
+    {
+      'next' => reply[0],
+      'entries' => reply[1]
+    }
+  }
+
   HashifyStreamPendings = lambda { |reply|
     {
       'size' => reply[0],
@@ -3529,6 +3811,21 @@ class Redis
       end
     end
   end
+
+  def _normalize_move_wheres(where_source, where_destination)
+    where_source      = where_source.to_s.upcase
+    where_destination = where_destination.to_s.upcase
+
+    if where_source != "LEFT" && where_source != "RIGHT"
+      raise ArgumentError, "where_source must be 'LEFT' or 'RIGHT'"
+    end
+
+    if where_destination != "LEFT" && where_destination != "RIGHT"
+      raise ArgumentError, "where_destination must be 'LEFT' or 'RIGHT'"
+    end
+
+    [where_source, where_destination]
+  end
 end
 
 require_relative "redis/version"
diff --git a/lib/redis/client.rb b/lib/redis/client.rb
index f01512f95e7732aaa6daa7d8f7f24101bc58509f..69cbe4de24132d7bcf54c4515cea481b24782fc7 100644
--- a/lib/redis/client.rb
+++ b/lib/redis/client.rb
@@ -17,6 +17,7 @@ class Redis
       write_timeout: nil,
       connect_timeout: nil,
       timeout: 5.0,
+      username: nil,
       password: nil,
       db: 0,
       driver: nil,
@@ -61,6 +62,10 @@ class Redis
       @options[:read_timeout]
     end
 
+    def username
+      @options[:username]
+    end
+
     def password
       @options[:password]
     end
@@ -110,7 +115,33 @@ class Redis
       # Don't try to reconnect when the connection is fresh
       with_reconnect(false) do
         establish_connection
-        call [:auth, password] if password
+        if password
+          if username
+            begin
+              call [:auth, username, password]
+            rescue CommandError => err # Likely on Redis < 6
+              if err.message.match?(/ERR wrong number of arguments for \'auth\' command/)
+                call [:auth, password]
+              elsif err.message.match?(/WRONGPASS invalid username-password pair/)
+                begin
+                  call [:auth, password]
+                rescue CommandError
+                  raise err
+                end
+                ::Kernel.warn(
+                  "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
+                  " the provided password was for the default user. This will start failing in redis-rb 4.6."
+                )
+              else
+                raise
+              end
+            end
+          else
+            call [:auth, password]
+          end
+        end
+
+        call [:readonly] if @options[:readonly]
         call [:select, db] if db != 0
         call [:client, :setname, @options[:id]] if @options[:id]
         @connector.check(self)
@@ -131,7 +162,7 @@ class Redis
       reply = process([command]) { read }
       raise reply if reply.is_a?(CommandError)
 
-      if block_given?
+      if block_given? && reply != 'QUEUED'
         yield reply
       else
         reply
@@ -434,7 +465,8 @@ class Redis
           defaults[:scheme]   = uri.scheme
           defaults[:host]     = uri.host if uri.host
           defaults[:port]     = uri.port if uri.port
-          defaults[:password] = CGI.unescape(uri.password) if uri.password
+          defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
+          defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
           defaults[:db]       = uri.path[1..-1].to_i if uri.path
           defaults[:role] = :master
         else
@@ -510,7 +542,7 @@ class Redis
           require_relative "connection/#{driver}"
         rescue LoadError, NameError
           begin
-            require "connection/#{driver}"
+            require "redis/connection/#{driver}"
           rescue LoadError, NameError => error
             raise "Cannot load driver #{driver.inspect}: #{error.message}"
           end
@@ -579,6 +611,7 @@ class Redis
             client = Client.new(@options.merge({
                                                  host: sentinel[:host] || sentinel["host"],
                                                  port: sentinel[:port] || sentinel["port"],
+                                                 username: sentinel[:username] || sentinel["username"],
                                                  password: sentinel[:password] || sentinel["password"],
                                                  reconnect_attempts: 0
                                                }))
diff --git a/lib/redis/cluster.rb b/lib/redis/cluster.rb
index b8443192f8a11e6a9939481deb32945813586ee3..a3ffdc7ae93ac8d6046dac9b1722b5af15f8b741 100644
--- a/lib/redis/cluster.rb
+++ b/lib/redis/cluster.rb
@@ -78,11 +78,13 @@ class Redis
     end
 
     def call_pipeline(pipeline)
-      node_keys, command_keys = extract_keys_in_pipeline(pipeline)
-      raise CrossSlotPipeliningError, command_keys if node_keys.size > 1
+      node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq
+      if node_keys.size > 1
+        raise(CrossSlotPipeliningError,
+              pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
+      end
 
-      node = find_node(node_keys.first)
-      try_send(node, :call_pipeline, pipeline)
+      try_send(find_node(node_keys.first), :call_pipeline, pipeline)
     end
 
     def call_with_timeout(command, timeout, &block)
@@ -128,7 +130,7 @@ class Redis
     def send_command(command, &block)
       cmd = command.first.to_s.downcase
       case cmd
-      when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
+      when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
         @node.call_all(command, &block).first
       when 'flushall', 'flushdb'
         @node.call_master(command, &block).first
@@ -253,14 +255,14 @@ class Redis
       find_node(node_key)
     end
 
-    def find_node_key(command)
+    def find_node_key(command, primary_only: false)
       key = @command.extract_first_key(command)
       return if key.empty?
 
       slot = KeySlotConverter.convert(key)
       return unless @slot.exists?(slot)
 
-      if @command.should_send_to_master?(command)
+      if @command.should_send_to_master?(command) || primary_only
         @slot.find_node_key_of_master(slot)
       else
         @slot.find_node_key_of_slave(slot)
@@ -285,11 +287,5 @@ class Redis
       @node.map(&:disconnect)
       @node, @slot = fetch_cluster_info!(@option)
     end
-
-    def extract_keys_in_pipeline(pipeline)
-      node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
-      command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
-      [node_keys, command_keys]
-    end
   end
 end
diff --git a/lib/redis/cluster/command_loader.rb b/lib/redis/cluster/command_loader.rb
index 574212d356be4fb7ec88dc42ed41cc98c231d689..99673b6fba1e2998acb82d22905a09cc6f9ca492 100644
--- a/lib/redis/cluster/command_loader.rb
+++ b/lib/redis/cluster/command_loader.rb
@@ -10,22 +10,21 @@ class Redis
       module_function
 
       def load(nodes)
-        details = {}
-
         nodes.each do |node|
-          details = fetch_command_details(node)
-          details.empty? ? next : break
+          begin
+            return fetch_command_details(node)
+          rescue CannotConnectError, ConnectionError, CommandError
+            next # can retry on another node
+          end
         end
 
-        details
+        raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
       end
 
       def fetch_command_details(node)
         node.call(%i[command]).map do |reply|
           [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
         end.to_h
-      rescue CannotConnectError, ConnectionError, CommandError
-        {} # can retry on another node
       end
 
       private_class_method :fetch_command_details
diff --git a/lib/redis/cluster/node.rb b/lib/redis/cluster/node.rb
index 6040ea8eab39140e93f6aeef3424b970775cc202..f3be41461191263c7c8e7c7344b88ea7666ee579 100644
--- a/lib/redis/cluster/node.rb
+++ b/lib/redis/cluster/node.rb
@@ -76,8 +76,9 @@ class Redis
         clients = options.map do |node_key, option|
           next if replica_disabled? && slave?(node_key)
 
+          option = option.merge(readonly: true) if slave?(node_key)
+
           client = Client.new(option)
-          client.call(%i[readonly]) if slave?(node_key)
           [node_key, client]
         end
 
diff --git a/lib/redis/cluster/option.rb b/lib/redis/cluster/option.rb
index 6641b30816bab66b01b12e72f18c9d28bada4afd..f0fdca2f749e805701078c6bf199165c47bd9e65 100644
--- a/lib/redis/cluster/option.rb
+++ b/lib/redis/cluster/option.rb
@@ -18,6 +18,7 @@ class Redis
         @node_opts = build_node_options(node_addrs)
         @replica = options.delete(:replica) == true
         add_common_node_option_if_needed(options, @node_opts, :scheme)
+        add_common_node_option_if_needed(options, @node_opts, :username)
         add_common_node_option_if_needed(options, @node_opts, :password)
         @options = options
       end
@@ -63,7 +64,9 @@ class Redis
         raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
 
         db = uri.path.split('/')[1]&.to_i
-        { scheme: uri.scheme, password: uri.password, host: uri.host, port: uri.port, db: db }.reject { |_, v| v.nil? }
+
+        { scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
+          .reject { |_, v| v.nil? || v == '' }
       rescue URI::InvalidURIError => err
         raise InvalidClientOptionError, err.message
       end
@@ -79,7 +82,7 @@ class Redis
 
       # Redis cluster node returns only host and port information.
       # So we should complement additional information such as:
-      #   scheme, password and so on.
+      #   scheme, username, password and so on.
       def add_common_node_option_if_needed(options, node_opts, key)
         return options if options[key].nil? && node_opts.first[key].nil?
 
diff --git a/lib/redis/connection/command_helper.rb b/lib/redis/connection/command_helper.rb
index 04b89113a6023c4e36319b9cfd2e07b0e7a68457..f14f90a875b4de9e653aa6bd739969d609353de4 100644
--- a/lib/redis/connection/command_helper.rb
+++ b/lib/redis/connection/command_helper.rb
@@ -12,11 +12,13 @@ class Redis
           if i.is_a? Array
             i.each do |j|
               j = j.to_s
+              j = j.encoding == Encoding::BINARY ? j : j.b
               command << "$#{j.bytesize}"
               command << j
             end
           else
             i = i.to_s
+            i = i.encoding == Encoding::BINARY ? i : i.b
             command << "$#{i.bytesize}"
             command << i
           end
diff --git a/lib/redis/connection/ruby.rb b/lib/redis/connection/ruby.rb
index 7ef5ca9fa6b535caad10bb78bd1a7532e8402e95..b623a1f3e37cbaaa15221c9ecb105f793c09197c 100644
--- a/lib/redis/connection/ruby.rb
+++ b/lib/redis/connection/ruby.rb
@@ -21,7 +21,7 @@ class Redis
         super(*args)
 
         @timeout = @write_timeout = nil
-        @buffer = "".dup
+        @buffer = "".b
       end
 
       def timeout=(timeout)
@@ -35,7 +35,8 @@ class Redis
       def read(nbytes)
         result = @buffer.slice!(0, nbytes)
 
-        result << _read_from_socket(nbytes - result.bytesize) while result.bytesize < nbytes
+        buffer = String.new(capacity: nbytes, encoding: Encoding::ASCII_8BIT)
+        result << _read_from_socket(nbytes - result.bytesize, buffer) while result.bytesize < nbytes
 
         result
       end
@@ -48,9 +49,9 @@ class Redis
         @buffer.slice!(0, crlf + CRLF.bytesize)
       end
 
-      def _read_from_socket(nbytes)
+      def _read_from_socket(nbytes, buffer = nil)
         loop do
-          case chunk = read_nonblock(nbytes, exception: false)
+          case chunk = read_nonblock(nbytes, buffer, exception: false)
           when :wait_readable
             unless wait_readable(@timeout)
               raise Redis::TimeoutError
diff --git a/lib/redis/distributed.rb b/lib/redis/distributed.rb
index 0ffaa2bd29c8d6b93951e9c65c796718332f5ab3..0ae33d9f1ffc7a0e222598e7d97ce561dd84a073 100644
--- a/lib/redis/distributed.rb
+++ b/lib/redis/distributed.rb
@@ -316,6 +316,16 @@ class Redis
       node_for(key).get(key)
     end
 
+    # Get the value of a key and delete it.
+    def getdel(key)
+      node_for(key).getdel(key)
+    end
+
+    # Get the value of a key and sets its time to live based on options.
+    def getex(key, **options)
+      node_for(key).getex(key, **options)
+    end
+
     # Get the values of all the given keys as an Array.
     def mget(*keys)
       mapped_mget(*keys).values_at(*keys)
@@ -393,6 +403,21 @@ class Redis
       node_for(key).llen(key)
     end
 
+    # Remove the first/last element in a list, append/prepend it to another list and return it.
+    def lmove(source, destination, where_source, where_destination)
+      ensure_same_node(:lmove, [source, destination]) do |node|
+        node.lmove(source, destination, where_source, where_destination)
+      end
+    end
+
+    # Remove the first/last element in a list and append/prepend it
+    # to another list and return it, or block until one is available.
+    def blmove(source, destination, where_source, where_destination, timeout: 0)
+      ensure_same_node(:lmove, [source, destination]) do |node|
+        node.blmove(source, destination, where_source, where_destination, timeout: timeout)
+      end
+    end
+
     # Prepend one or more values to a list.
     def lpush(key, value)
       node_for(key).lpush(key, value)
@@ -413,14 +438,14 @@ class Redis
       node_for(key).rpushx(key, value)
     end
 
-    # Remove and get the first element in a list.
-    def lpop(key)
-      node_for(key).lpop(key)
+    # Remove and get the first elements in a list.
+    def lpop(key, count = nil)
+      node_for(key).lpop(key, count)
     end
 
-    # Remove and get the last element in a list.
-    def rpop(key)
-      node_for(key).rpop(key)
+    # Remove and get the last elements in a list.
+    def rpop(key, count = nil)
+      node_for(key).rpop(key, count)
     end
 
     # Remove the last element in a list, append it to another list and return
@@ -542,6 +567,11 @@ class Redis
       node_for(key).sismember(key, member)
     end
 
+    # Determine if multiple values are members of a set.
+    def smismember(key, *members)
+      node_for(key).smismember(key, *members)
+    end
+
     # Get all the members in a set.
     def smembers(key)
       node_for(key).smembers(key)
@@ -626,6 +656,16 @@ class Redis
       node_for(key).zscore(key, member)
     end
 
+    # Get one or more random members from a sorted set.
+    def zrandmember(key, count = nil, **options)
+      node_for(key).zrandmember(key, count, **options)
+    end
+
+    # Get the scores associated with the given members in a sorted set.
+    def zmscore(key, *members)
+      node_for(key).zmscore(key, *members)
+    end
+
     # Return a range of members in a sorted set, by index.
     def zrange(key, start, stop, **options)
       node_for(key).zrange(key, start, stop, **options)
@@ -674,6 +714,13 @@ class Redis
       node_for(key).zcount(key, min, max)
     end
 
+    # Get the intersection of multiple sorted sets
+    def zinter(*keys, **options)
+      ensure_same_node(:zinter, keys) do |node|
+        node.zinter(*keys, **options)
+      end
+    end
+
     # Intersect multiple sorted sets and store the resulting sorted set in a new
     # key.
     def zinterstore(destination, keys, **options)
diff --git a/lib/redis/version.rb b/lib/redis/version.rb
index 1a12c42dab02295aa7bec146c1b1679b7c0de5dd..abcaeb88dfd9507f689a1a975ef30fb1bb75a6d1 100644
--- a/lib/redis/version.rb
+++ b/lib/redis/version.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 class Redis
-  VERSION = '4.2.5'
+  VERSION = '4.5.1'
 end
diff --git a/makefile b/makefile
index 307e75c30921f25c848278a51529df16d5a96035..c803066b17b56ebbbbb325611bf9eba2976432d6 100644
--- a/makefile
+++ b/makefile
@@ -1,4 +1,4 @@
-REDIS_BRANCH       ?= 6.0
+REDIS_BRANCH       ?= 6.2
 TMP                := tmp
 BUILD_DIR          := ${TMP}/cache/redis-${REDIS_BRANCH}
 TARBALL            := ${TMP}/redis-${REDIS_BRANCH}.tar.gz
diff --git a/redis.gemspec b/redis.gemspec
index b220c492da28c2c27b29cd9dabe269683832ec44..afdcaab8b053b302ee63272b3efb11f1e892f210 100644
--- a/redis.gemspec
+++ b/redis.gemspec
@@ -43,7 +43,7 @@ Gem::Specification.new do |s|
   s.files         = Dir["CHANGELOG.md", "LICENSE", "README.md", "lib/**/*"]
   s.executables   = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) }
 
-  s.required_ruby_version = '>= 2.3.0'
+  s.required_ruby_version = '>= 2.4.0'
 
   s.add_development_dependency("em-synchrony")
   s.add_development_dependency("hiredis")
diff --git a/test/blocking_commands_test.rb b/test/blocking_commands_test.rb
index c32f11eef6e1a9247b150ac8fed8e38eb6215a56..5df8e6c5353d88b7e32646d5e66eac4fbdc850f6 100644
--- a/test/blocking_commands_test.rb
+++ b/test/blocking_commands_test.rb
@@ -21,6 +21,14 @@ class TestBlockingCommands < Minitest::Test
     end
   end
 
+  def test_blmove_disable_client_timeout
+    target_version "6.2" do
+      assert_takes_longer_than_client_timeout do |r|
+        assert_equal '0', r.blmove('foo', 'bar', 'LEFT', 'RIGHT')
+      end
+    end
+  end
+
   def test_blpop_disable_client_timeout
     assert_takes_longer_than_client_timeout do |r|
       assert_equal %w[foo 0], r.blpop('foo')
diff --git a/test/client_test.rb b/test/client_test.rb
index 30f7e2cff916b882db04b4044a08adf784dfd7f9..7bcbb25cd9584a9e5a3c8a2075f20ecec073add6 100644
--- a/test/client_test.rb
+++ b/test/client_test.rb
@@ -73,4 +73,13 @@ class TestClient < Minitest::Test
     end
     assert_equal 'Error connecting to Redis on 127.0.0.5:999 (Errno::ECONNREFUSED)', error.message
   end
+
+  def test_mixed_encoding
+    r.call("MSET", "fée", "\x00\xFF".b, "じ案".encode(Encoding::SHIFT_JIS), "\t".encode(Encoding::ASCII))
+    assert_equal "\x00\xFF", r.call("GET", "fée")
+    assert_equal "\t", r.call("GET", "じ案".encode(Encoding::SHIFT_JIS))
+
+    r.call("SET", "\x00\xFF", "fée")
+    assert_equal "fée", r.call("GET", "\x00\xFF".b)
+  end
 end
diff --git a/test/cluster_client_internals_test.rb b/test/cluster_client_internals_test.rb
index 501d75806c09a71ce8dc5b037766de3fbed306bc..01c8acaebb0a319d359193a825dcd944fe9b84d3 100644
--- a/test/cluster_client_internals_test.rb
+++ b/test/cluster_client_internals_test.rb
@@ -74,4 +74,23 @@ class TestClusterClientInternals < Minitest::Test
 
     assert_equal expected, redis.connection
   end
+
+  def test_acl_auth_success
+    target_version "6.0.0" do
+      with_acl do |username, password|
+        r = _new_client(cluster: DEFAULT_PORTS.map { |port| "redis://#{username}:#{password}@#{DEFAULT_HOST}:#{port}" })
+        assert_equal('PONG', r.ping)
+      end
+    end
+  end
+
+  def test_acl_auth_failure
+    target_version "6.0.0" do
+      with_acl do |username, _|
+        assert_raises(Redis::CannotConnectError) do
+          _new_client(cluster: DEFAULT_PORTS.map { |port| "redis://#{username}:wrongpassword@#{DEFAULT_HOST}:#{port}" })
+        end
+      end
+    end
+  end
 end
diff --git a/test/cluster_client_key_hash_tags_test.rb b/test/cluster_client_key_hash_tags_test.rb
index 8ff4d301a3897ea5155b3ecc9ee8f4a15af72979..81e87165fb24169294fad282f6a9774551f02eec 100644
--- a/test/cluster_client_key_hash_tags_test.rb
+++ b/test/cluster_client_key_hash_tags_test.rb
@@ -6,8 +6,8 @@ require_relative 'helper'
 class TestClusterClientKeyHashTags < Minitest::Test
   include Helper::Cluster
 
-  def build_described_class
-    option = Redis::Cluster::Option.new(cluster: ['redis://127.0.0.1:7000'])
+  def build_described_class(urls = ['redis://127.0.0.1:7000'])
+    option = Redis::Cluster::Option.new(cluster: urls)
     node = Redis::Cluster::Node.new(option.per_node_key)
     details = Redis::Cluster::CommandLoader.load(node)
     Redis::Cluster::Command.new(details)
@@ -85,4 +85,15 @@ class TestClusterClientKeyHashTags < Minitest::Test
       assert_equal false, described_class.should_send_to_slave?([:info])
     end
   end
+
+  def test_cannot_build_details_from_bad_urls
+    assert_raises(Redis::CannotConnectError) do
+      build_described_class(['redis://127.0.0.1:7006'])
+    end
+  end
+
+  def test_builds_details_from_a_mix_of_good_and_bad_urls
+    described_class = build_described_class(['redis://127.0.0.1:7006', 'redis://127.0.0.1:7000'])
+    assert_equal 'dogs:1', described_class.extract_first_key(%w[get dogs:1])
+  end
 end
diff --git a/test/cluster_client_options_test.rb b/test/cluster_client_options_test.rb
index 24d7df41f6a9b3f8a3b78184bbe35e70c61633c5..cc6e4d6e9b18bea7059fd1c6a0df9c85abd12788 100644
--- a/test/cluster_client_options_test.rb
+++ b/test/cluster_client_options_test.rb
@@ -20,11 +20,14 @@ class TestClusterClientOptions < Minitest::Test
     assert_equal false, option.use_replica?
 
     option = Redis::Cluster::Option.new(cluster: %w[rediss://johndoe:foobar@127.0.0.1:7000/1/namespace])
-    assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', password: 'foobar', host: '127.0.0.1', port: 7000, db: 1 } }, option.per_node_key)
+    assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', username: 'johndoe', password: 'foobar', host: '127.0.0.1', port: 7000, db: 1 } }, option.per_node_key)
 
     option = Redis::Cluster::Option.new(cluster: %w[rediss://127.0.0.1:7000], scheme: 'redis')
     assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
 
+    option = Redis::Cluster::Option.new(cluster: %w[redis://bazzap:@127.0.0.1:7000], username: 'foobar')
+    assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', username: 'bazzap', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
+
     option = Redis::Cluster::Option.new(cluster: %w[redis://:bazzap@127.0.0.1:7000], password: 'foobar')
     assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', password: 'bazzap', host: '127.0.0.1', port: 7000 } }, option.per_node_key)
 
diff --git a/test/cluster_client_pipelining_test.rb b/test/cluster_client_pipelining_test.rb
index 56f2c693cb2acffb35f36dcc0baf11e568eb70b1..85a54576f90359b7fff8f96aeaba4cd0b3f61081 100644
--- a/test/cluster_client_pipelining_test.rb
+++ b/test/cluster_client_pipelining_test.rb
@@ -56,4 +56,17 @@ class TestClusterClientPipelining < Minitest::Test
       end
     end
   end
+
+  def test_pipelining_with_multiple_replicas
+    rc = build_another_client(replica: true)
+    rc.instance_variable_get(:@client).instance_variable_get(:@slot).instance_variable_get(:@map).each do |_, v|
+      v[:slaves] << v[:master] if v[:slaves].size < 2 # reproducing multiple replicas
+    end
+
+    rc.pipelined do |r|
+      10.times { r.get('key1') }
+    end
+
+    rc.close
+  end
 end
diff --git a/test/cluster_commands_on_connection_test.rb b/test/cluster_commands_on_connection_test.rb
index b4129695a312bfbc4839a7fb02aff24f9a7c6ee0..24c093889888df88fe351cb33708b1b5844946d1 100644
--- a/test/cluster_commands_on_connection_test.rb
+++ b/test/cluster_commands_on_connection_test.rb
@@ -1,17 +1,13 @@
 # frozen_string_literal: true
 
 require_relative 'helper'
+require 'lint/authentication'
 
 # ruby -w -Itest test/cluster_commands_on_connection_test.rb
 # @see https://redis.io/commands#connection
 class TestClusterCommandsOnConnection < Minitest::Test
   include Helper::Cluster
-
-  def test_auth
-    redis_cluster_mock(auth: ->(*_) { '+OK' }) do |redis|
-      assert_equal 'OK', redis.auth('my-password-123')
-    end
-  end
+  include Lint::Authentication
 
   def test_echo
     assert_equal 'hogehoge', redis.echo('hogehoge')
@@ -37,4 +33,8 @@ class TestClusterCommandsOnConnection < Minitest::Test
       redis.swapdb(1, 2)
     end
   end
+
+  def mock(*args, &block)
+    redis_cluster_mock(*args, &block)
+  end
 end
diff --git a/test/cluster_commands_on_lists_test.rb b/test/cluster_commands_on_lists_test.rb
index ea6e9704eeecd10f6f7625c9f213a17adb7fdde2..a483c8294dca4947c11d238a4b401dae105b327d 100644
--- a/test/cluster_commands_on_lists_test.rb
+++ b/test/cluster_commands_on_lists_test.rb
@@ -9,6 +9,12 @@ class TestClusterCommandsOnLists < Minitest::Test
   include Helper::Cluster
   include Lint::Lists
 
+  def test_lmove
+    target_version "6.2" do
+      assert_raises(Redis::CommandError) { super }
+    end
+  end
+
   def test_rpoplpush
     assert_raises(Redis::CommandError) { super }
   end
diff --git a/test/cluster_commands_on_server_test.rb b/test/cluster_commands_on_server_test.rb
index b135de2cb6a8a3537bc8a1a8502ab15ea56776df..2f7faf0137246159c8a8d6c5a3d3eafa74576c6f 100644
--- a/test/cluster_commands_on_server_test.rb
+++ b/test/cluster_commands_on_server_test.rb
@@ -44,6 +44,7 @@ class TestClusterCommandsOnServer < Minitest::Test
     actual = a_client_info.keys.sort
     expected = %w[addr age cmd db events fd flags id idle multi name obl oll omem psub qbuf qbuf-free sub]
     expected << 'user' << 'argv-mem' << 'tot-mem' if version >= '6'
+    expected << 'laddr' << 'redir' if version >= '6.2'
     assert_equal expected.sort, actual.sort
   end
 
@@ -79,10 +80,16 @@ class TestClusterCommandsOnServer < Minitest::Test
   end
 
   def test_command_info
+    eval_command_flags = if version >= '6.2'
+      %w[noscript skip_monitor may_replicate movablekeys]
+    else
+      %w[noscript movablekeys]
+    end
+
     expected = [
       ['get', 2, %w[readonly fast], 1, 1, 1],
       ['set', -3, %w[write denyoom], 1, 1, 1],
-      ['eval', -3, %w[noscript movablekeys], 0, 0, 0]
+      ['eval', -3, eval_command_flags, 0, 0, 0]
     ]
     if version >= '6'
       expected[0] << ["@read", "@string", "@fast"]
diff --git a/test/cluster_commands_on_sorted_sets_test.rb b/test/cluster_commands_on_sorted_sets_test.rb
index 3c37b429af727b67dbe91de515716eca79a953ef..87da66ee879cb15e8edf908e099742f3eab2252b 100644
--- a/test/cluster_commands_on_sorted_sets_test.rb
+++ b/test/cluster_commands_on_sorted_sets_test.rb
@@ -9,6 +9,18 @@ class TestClusterCommandsOnSortedSets < Minitest::Test
   include Helper::Cluster
   include Lint::SortedSets
 
+  def test_zinter
+    assert_raises(Redis::CommandError) { super }
+  end
+
+  def test_zinter_with_aggregate
+    assert_raises(Redis::CommandError) { super }
+  end
+
+  def test_zinter_with_weights
+    assert_raises(Redis::CommandError) { super }
+  end
+
   def test_zinterstore
     assert_raises(Redis::CommandError) { super }
   end
diff --git a/test/commands_on_value_types_test.rb b/test/commands_on_value_types_test.rb
index fddc5f2ac1922d749c2c460546f7890c1bd71b86..60db89da1febca013337e437b0c721784bf592c1 100644
--- a/test/commands_on_value_types_test.rb
+++ b/test/commands_on_value_types_test.rb
@@ -14,6 +14,8 @@ class TestCommandsOnValueTypes < Minitest::Test
 
     assert_equal ["bar", "baz", "foo"], r.keys("*").sort
 
+    assert_equal 0, r.del("")
+
     assert_equal 1, r.del("foo")
 
     assert_equal ["bar", "baz"], r.keys("*").sort
@@ -30,6 +32,8 @@ class TestCommandsOnValueTypes < Minitest::Test
 
     assert_equal ["bar", "baz", "foo"], r.keys("*").sort
 
+    assert_equal 0, r.del([])
+
     assert_equal 1, r.del(["foo"])
 
     assert_equal ["bar", "baz"], r.keys("*").sort
diff --git a/test/connection_handling_test.rb b/test/connection_handling_test.rb
index 7949a229721b9b27a745a0b415b5cbaee2dd4a71..1e750f09922ca0c77de2a9e3413745e76ff8d260 100644
--- a/test/connection_handling_test.rb
+++ b/test/connection_handling_test.rb
@@ -1,20 +1,11 @@
 # frozen_string_literal: true
 
 require_relative "helper"
+require 'lint/authentication'
 
 class TestConnectionHandling < Minitest::Test
   include Helper::Client
-
-  def test_auth
-    commands = {
-      auth: ->(password) { @auth = password; "+OK" },
-      get: ->(_key) { @auth == "secret" ? "$3\r\nbar" : "$-1" }
-    }
-
-    redis_mock(commands, password: "secret") do |redis|
-      assert_equal "bar", redis.get("foo")
-    end
-  end
+  include Lint::Authentication
 
   def test_id
     commands = {
diff --git a/test/connection_test.rb b/test/connection_test.rb
index 933bd6155ddd69bf0d6d69b2bc82d144cd0afc83..5c8bf600926d0480cc27db355d95bcaec1a11307 100644
--- a/test/connection_test.rb
+++ b/test/connection_test.rb
@@ -9,6 +9,34 @@ class TestConnection < Minitest::Test
     assert_equal "#<Redis client v#{Redis::VERSION} for redis://127.0.0.1:#{PORT}/15>", r.inspect
   end
 
+  def test_connection_with_user_and_password
+    target_version "6.0" do
+      with_acl do |username, password|
+        redis = Redis.new(OPTIONS.merge(username: username, password: password))
+        assert_equal "PONG", redis.ping
+      end
+    end
+  end
+
+  def test_connection_with_default_user_and_password
+    target_version "6.0" do
+      with_default_user_password do |_username, password|
+        redis = Redis.new(OPTIONS.merge(password: password))
+        assert_equal "PONG", redis.ping
+      end
+    end
+  end
+
+  def test_connection_with_wrong_user_and_password
+    target_version "6.0" do
+      with_default_user_password do |_username, password|
+        Kernel.expects(:warn).once
+        redis = Redis.new(OPTIONS.merge(username: "does-not-exist", password: password))
+        assert_equal "PONG", redis.ping
+      end
+    end
+  end
+
   def test_connection_information
     assert_equal "127.0.0.1",                 r.connection.fetch(:host)
     assert_equal 6381,                        r.connection.fetch(:port)
diff --git a/test/distributed_blocking_commands_test.rb b/test/distributed_blocking_commands_test.rb
index efa86918f5e26794a1ae757cb9cb939a5bb2fb24..a2c0d1dc8957a7d39965b9cc1a963fd32a8fb68c 100644
--- a/test/distributed_blocking_commands_test.rb
+++ b/test/distributed_blocking_commands_test.rb
@@ -7,6 +7,14 @@ class TestDistributedBlockingCommands < Minitest::Test
   include Helper::Distributed
   include Lint::BlockingCommands
 
+  def test_blmove_raises
+    target_version "6.2" do
+      assert_raises(Redis::Distributed::CannotDistribute) do
+        r.blmove('foo', 'bar', 'LEFT', 'RIGHT')
+      end
+    end
+  end
+
   def test_blpop_raises
     assert_raises(Redis::Distributed::CannotDistribute) do
       r.blpop(%w[foo bar])
diff --git a/test/distributed_commands_on_lists_test.rb b/test/distributed_commands_on_lists_test.rb
index c9039a059c5413fe1d91d505fa2bf8b44c9f7556..4c372340042d570fdbe3ad0caeb0f8f0c6c157c9 100644
--- a/test/distributed_commands_on_lists_test.rb
+++ b/test/distributed_commands_on_lists_test.rb
@@ -7,6 +7,14 @@ class TestDistributedCommandsOnLists < Minitest::Test
   include Helper::Distributed
   include Lint::Lists
 
+  def test_lmove
+    target_version "6.2" do
+      assert_raises Redis::Distributed::CannotDistribute do
+        r.lmove('foo', 'bar', 'LEFT', 'RIGHT')
+      end
+    end
+  end
+
   def test_rpoplpush
     assert_raises Redis::Distributed::CannotDistribute do
       r.rpoplpush('foo', 'bar')
diff --git a/test/distributed_commands_on_sorted_sets_test.rb b/test/distributed_commands_on_sorted_sets_test.rb
index 2e628364c8d1f9496b2b1a61e66a57227e4144a3..cd220bcf02769b9aab096d869f8e1f2afa0ad3b9 100644
--- a/test/distributed_commands_on_sorted_sets_test.rb
+++ b/test/distributed_commands_on_sorted_sets_test.rb
@@ -7,6 +7,18 @@ class TestDistributedCommandsOnSortedSets < Minitest::Test
   include Helper::Distributed
   include Lint::SortedSets
 
+  def test_zinter
+    assert_raises(Redis::Distributed::CannotDistribute) { super }
+  end
+
+  def test_zinter_with_aggregate
+    assert_raises(Redis::Distributed::CannotDistribute) { super }
+  end
+
+  def test_zinter_with_weights
+    assert_raises(Redis::Distributed::CannotDistribute) { super }
+  end
+
   def test_zinterstore
     assert_raises(Redis::Distributed::CannotDistribute) { super }
   end
diff --git a/test/distributed_commands_requiring_clustering_test.rb b/test/distributed_commands_requiring_clustering_test.rb
index 35bd7988fbb7303b7560752c6b03863a509e7ba8..7ffbe532d18188447add80325d213efa6b5cd431 100644
--- a/test/distributed_commands_requiring_clustering_test.rb
+++ b/test/distributed_commands_requiring_clustering_test.rb
@@ -23,6 +23,19 @@ class TestDistributedCommandsRequiringClustering < Minitest::Test
     assert_equal "s2", r.get("{qux}bar")
   end
 
+  def test_lmove
+    target_version "6.2" do
+      r.rpush("{qux}foo", "s1")
+      r.rpush("{qux}foo", "s2")
+      r.rpush("{qux}bar", "s3")
+      r.rpush("{qux}bar", "s4")
+
+      assert_equal "s1", r.lmove("{qux}foo", "{qux}bar", "LEFT", "RIGHT")
+      assert_equal ["s2"], r.lrange("{qux}foo", 0, -1)
+      assert_equal ["s3", "s4", "s1"], r.lrange("{qux}bar", 0, -1)
+    end
+  end
+
   def test_brpoplpush
     r.rpush "{qux}foo", "s1"
     r.rpush "{qux}foo", "s2"
diff --git a/test/helper.rb b/test/helper.rb
index 81be94b083db16c22e0097882519ebd3545b7c11..0a98354db7224cb8e8206417702c93c96eb47c2e 100644
--- a/test/helper.rb
+++ b/test/helper.rb
@@ -165,6 +165,26 @@ module Helper
     def version
       Version.new(redis.info['redis_version'])
     end
+
+    def with_acl
+      admin = _new_client
+      admin.acl('SETUSER', 'johndoe', 'on',
+                '+ping', '+select', '+command', '+cluster|slots', '+cluster|nodes',
+                '>mysecret')
+      yield('johndoe', 'mysecret')
+    ensure
+      admin.acl('DELUSER', 'johndoe')
+      admin.close
+    end
+
+    def with_default_user_password
+      admin = _new_client
+      admin.acl('SETUSER', 'default', '>mysecret')
+      yield('default', 'mysecret')
+    ensure
+      admin.acl('SETUSER', 'default', 'nopass')
+      admin.close
+    end
   end
 
   module Client
diff --git a/test/lint/authentication.rb b/test/lint/authentication.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6dad12ef20e1f2a90c2c97710d12619faf1393c7
--- /dev/null
+++ b/test/lint/authentication.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Lint
+  module Authentication
+    def test_auth_with_password
+      mock(auth: ->(*_) { '+OK' }) do |r|
+        assert_equal 'OK', r.auth('mysecret')
+      end
+
+      mock(auth: ->(*_) { '-ERR some error' }) do |r|
+        assert_raises(Redis::BaseError) { r.auth('mysecret') }
+      end
+    end
+
+    def test_auth_for_acl
+      target_version "6.0.0" do
+        with_acl do |username, password|
+          assert_raises(Redis::BaseError) { redis.auth(username, 'wrongpassword') }
+          assert_equal 'OK', redis.auth(username, password)
+          assert_equal 'PONG', redis.ping
+          assert_raises(Redis::BaseError) { redis.echo('foo') }
+        end
+      end
+    end
+
+    def mock(*args, &block)
+      redis_mock(*args, &block)
+    end
+  end
+end
diff --git a/test/lint/blocking_commands.rb b/test/lint/blocking_commands.rb
index 11bbe0bf29b64ef6a062cb437d4529835a346e92..3500b53d05d507c5e6e7e62e8bf0f6556984cbd0 100644
--- a/test/lint/blocking_commands.rb
+++ b/test/lint/blocking_commands.rb
@@ -32,6 +32,10 @@ module Lint
 
     def build_mock_commands(options = {})
       {
+        blmove: lambda do |*args|
+          sleep options[:delay] if options.key?(:delay)
+          to_protocol(args.last)
+        end,
         blpop: lambda do |*args|
           sleep options[:delay] if options.key?(:delay)
           to_protocol([args.first, args.last])
@@ -55,6 +59,23 @@ module Lint
       }
     end
 
+    def test_blmove
+      target_version "6.2" do
+        assert_equal 's1', r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT')
+        assert_equal ['s2'], r.lrange('{zap}foo', 0, -1)
+        assert_equal ['s1', 's2', 's1'], r.lrange('{zap}bar', 0, -1)
+      end
+    end
+
+    def test_blmove_timeout
+      target_version "6.2" do
+        mock do |r|
+          assert_equal '0', r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT')
+          assert_equal LOW_TIMEOUT.to_s, r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT', timeout: LOW_TIMEOUT)
+        end
+      end
+    end
+
     def test_blpop
       assert_equal ['{zap}foo', 's1'], r.blpop('{zap}foo')
       assert_equal ['{zap}foo', 's2'], r.blpop(['{zap}foo'])
@@ -166,6 +187,16 @@ module Lint
     end
 
     driver(:ruby, :hiredis) do
+      def test_blmove_socket_timeout
+        target_version "6.2" do
+          mock(delay: LOW_TIMEOUT * 5) do |r|
+            assert_raises(Redis::TimeoutError) do
+              r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT', timeout: LOW_TIMEOUT)
+            end
+          end
+        end
+      end
+
       def test_blpop_socket_timeout
         mock(delay: LOW_TIMEOUT * 5) do |r|
           assert_raises(Redis::TimeoutError) do
diff --git a/test/lint/lists.rb b/test/lint/lists.rb
index b0c5a2570d1e842d9d435bf048a2d3dbd4c17b0f..534ab8b8e971b31d821f3ea775607552aa0bb8ad 100644
--- a/test/lint/lists.rb
+++ b/test/lint/lists.rb
@@ -2,6 +2,33 @@
 
 module Lint
   module Lists
+    def test_lmove
+      target_version "6.2" do
+        r.lpush("foo", "s1")
+        r.lpush("foo", "s2") # foo = [s2, s1]
+        r.lpush("bar", "s3")
+        r.lpush("bar", "s4") # bar = [s4, s3]
+
+        assert_nil r.lmove("nonexistent", "foo", "LEFT", "LEFT")
+
+        assert_equal "s2", r.lmove("foo", "foo", "LEFT", "RIGHT") # foo = [s1, s2]
+        assert_equal "s1", r.lmove("foo", "foo", "LEFT", "LEFT") # foo = [s1, s2]
+
+        assert_equal "s1", r.lmove("foo", "bar", "LEFT", "RIGHT") # foo = [s2], bar = [s4, s3, s1]
+        assert_equal ["s2"], r.lrange("foo", 0, -1)
+        assert_equal ["s4", "s3", "s1"], r.lrange("bar", 0, -1)
+
+        assert_equal "s2", r.lmove("foo", "bar", "LEFT", "LEFT") # foo = [], bar = [s2, s4, s3, s1]
+        assert_nil r.lmove("foo", "bar", "LEFT", "LEFT") # foo = [], bar = [s2, s4, s3, s1]
+        assert_equal ["s2", "s4", "s3", "s1"], r.lrange("bar", 0, -1)
+
+        error = assert_raises(ArgumentError) do
+          r.lmove("foo", "bar", "LEFT", "MIDDLE")
+        end
+        assert_equal "where_destination must be 'LEFT' or 'RIGHT'", error.message
+      end
+    end
+
     def test_lpush
       r.lpush "foo", "s1"
       r.lpush "foo", "s2"
@@ -119,6 +146,17 @@ module Lint
       assert_equal 1, r.llen("foo")
     end
 
+    def test_lpop_count
+      target_version("6.2") do
+        r.rpush "foo", "s1"
+        r.rpush "foo", "s2"
+
+        assert_equal 2, r.llen("foo")
+        assert_equal ["s1", "s2"], r.lpop("foo", 2)
+        assert_equal 0, r.llen("foo")
+      end
+    end
+
     def test_rpop
       r.rpush "foo", "s1"
       r.rpush "foo", "s2"
@@ -128,6 +166,17 @@ module Lint
       assert_equal 1, r.llen("foo")
     end
 
+    def test_rpop_count
+      target_version("6.2") do
+        r.rpush "foo", "s1"
+        r.rpush "foo", "s2"
+
+        assert_equal 2, r.llen("foo")
+        assert_equal ["s2", "s1"], r.rpop("foo", 2)
+        assert_equal 0, r.llen("foo")
+      end
+    end
+
     def test_linsert
       r.rpush "foo", "s1"
       r.rpush "foo", "s3"
diff --git a/test/lint/sets.rb b/test/lint/sets.rb
index 56c167de2f771646b8a73c5fc249b216b7d3cf3e..c799ae6789c36894901004f54179cc487deaeeca 100644
--- a/test/lint/sets.rb
+++ b/test/lint/sets.rb
@@ -88,6 +88,18 @@ module Lint
       assert_equal false, r.sismember("foo", "s2")
     end
 
+    def test_smismember
+      target_version("6.2") do
+        assert_equal [false], r.smismember("foo", "s1")
+
+        r.sadd "foo", "s1"
+        assert_equal [true], r.smismember("foo", "s1")
+
+        r.sadd "foo", "s3"
+        assert_equal [true, false, true], r.smismember("foo", "s1", "s2", "s3")
+      end
+    end
+
     def test_smembers
       assert_equal [], r.smembers("foo")
 
diff --git a/test/lint/sorted_sets.rb b/test/lint/sorted_sets.rb
index 40a6f02eab194c154263b16c9ad4ff98a1cec7b4..e1863d80c8586f5520c05a99d85d27171dcb3678 100644
--- a/test/lint/sorted_sets.rb
+++ b/test/lint/sorted_sets.rb
@@ -45,6 +45,35 @@ module Lint
         # Incompatible options combination
         assert_raises(Redis::CommandError) { r.zadd("foo", 1, "s1", xx: true, nx: true) }
       end
+
+      target_version "6.2" do
+        # LT option
+        r.zadd("foo", 2, "s1")
+
+        r.zadd("foo", 3, "s1", lt: true)
+        assert_equal 2.0, r.zscore("foo", "s1")
+
+        r.zadd("foo", 1, "s1", lt: true)
+        assert_equal 1.0, r.zscore("foo", "s1")
+
+        assert_equal true, r.zadd("foo", 3, "s2", lt: true) # adds new member
+        r.del "foo"
+
+        # GT option
+        r.zadd("foo", 2, "s1")
+
+        r.zadd("foo", 1, "s1", gt: true)
+        assert_equal 2.0, r.zscore("foo", "s1")
+
+        r.zadd("foo", 3, "s1", gt: true)
+        assert_equal 3.0, r.zscore("foo", "s1")
+
+        assert_equal true, r.zadd("foo", 1, "s2", gt: true) # adds new member
+        r.del "foo"
+
+        # Incompatible options combination
+        assert_raises(Redis::CommandError) { r.zadd("foo", 1, "s1", nx: true, gt: true) }
+      end
     end
 
     def test_variadic_zadd
@@ -109,6 +138,28 @@ module Lint
         # Incompatible options combination
         assert_raises(Redis::CommandError) { r.zadd("foo", [1, "s1"], xx: true, nx: true) }
       end
+
+      target_version "6.2" do
+        # LT option
+        r.zadd("foo", 2, "s1")
+
+        assert_equal 1, r.zadd("foo", [3, "s1", 2, "s2"], lt: true, ch: true)
+        assert_equal 2.0, r.zscore("foo", "s1")
+
+        assert_equal 1, r.zadd("foo", [1, "s1"], lt: true, ch: true)
+
+        r.del "foo"
+
+        # GT option
+        r.zadd("foo", 2, "s1")
+
+        assert_equal 1, r.zadd("foo", [1, "s1", 2, "s2"], gt: true, ch: true)
+        assert_equal 2.0, r.zscore("foo", "s1")
+
+        assert_equal 1, r.zadd("foo", [3, "s1"], gt: true, ch: true)
+
+        r.del "foo"
+      end
     end
 
     def test_zrem
@@ -293,6 +344,48 @@ module Lint
       assert_equal(+Float::INFINITY, r.zscore("bar", "s2"))
     end
 
+    def test_zmscore
+      target_version("6.2") do
+        r.zadd "foo", 1, "s1"
+
+        assert_equal [1.0], r.zmscore("foo", "s1")
+        assert_equal [nil], r.zmscore("foo", "s2")
+
+        r.zadd "foo", "-inf", "s2"
+        r.zadd "foo", "+inf", "s3"
+        assert_equal [1.0, nil], r.zmscore("foo", "s1", "s4")
+        assert_equal [-Float::INFINITY, +Float::INFINITY], r.zmscore("foo", "s2", "s3")
+      end
+    end
+
+    def test_zrandmember
+      target_version("6.2") do
+        assert_nil r.zrandmember("foo")
+
+        r.zadd "foo", 1.0, "s1"
+        r.zrem "foo", "s1"
+        assert_nil r.zrandmember("foo")
+        assert_equal [], r.zrandmember("foo", 1)
+
+        r.zadd "foo", 1.0, "s1"
+        r.zadd "foo", 2.0, "s2"
+        r.zadd "foo", 3.0, "s3"
+
+        3.times do
+          assert ["s1", "s2", "s3"].include?(r.zrandmember("foo"))
+        end
+
+        assert_equal 2, r.zrandmember("foo", 2).size
+        assert_equal 3, r.zrandmember("foo", 4).size
+        assert_equal 5, r.zrandmember("foo", -5).size
+
+        r.zrandmember("foo", 2, with_scores: true).each do |(member, score)|
+          assert ["s1", "s2", "s3"].include?(member)
+          assert_instance_of Float, score
+        end
+      end
+    end
+
     def test_zremrangebyrank
       r.zadd "foo", 10, "s1"
       r.zadd "foo", 20, "s2"
@@ -432,6 +525,55 @@ module Lint
       assert_equal 5, r.zunionstore('{1}baz', %w[{1}foo {1}bar])
     end
 
+    def test_zinter
+      target_version("6.2") do
+        r.zadd 'foo', 1, 's1'
+        r.zadd 'bar', 2, 's1'
+        r.zadd 'foo', 3, 's3'
+        r.zadd 'bar', 4, 's4'
+
+        assert_equal ['s1'], r.zinter('foo', 'bar')
+        assert_equal [['s1', 3.0]], r.zinter('foo', 'bar', with_scores: true)
+      end
+    end
+
+    def test_zinter_with_weights
+      target_version("6.2") do
+        r.zadd 'foo', 1, 's1'
+        r.zadd 'foo', 2, 's2'
+        r.zadd 'foo', 3, 's3'
+        r.zadd 'bar', 20, 's2'
+        r.zadd 'bar', 30, 's3'
+        r.zadd 'bar', 40, 's4'
+
+        assert_equal %w[s2 s3], r.zinter('foo', 'bar')
+        assert_equal [['s2', 22.0], ['s3', 33.0]], r.zinter('foo', 'bar', with_scores: true)
+
+        assert_equal %w[s2 s3], r.zinter('foo', 'bar', weights: [10, 1])
+        assert_equal [['s2', 40.0], ['s3', 60.0]], r.zinter('foo', 'bar', weights: [10, 1], with_scores: true)
+      end
+    end
+
+    def test_zinter_with_aggregate
+      target_version("6.2") do
+        r.zadd 'foo', 1, 's1'
+        r.zadd 'foo', 2, 's2'
+        r.zadd 'foo', 3, 's3'
+        r.zadd 'bar', 20, 's2'
+        r.zadd 'bar', 30, 's3'
+        r.zadd 'bar', 40, 's4'
+
+        assert_equal %w[s2 s3], r.zinter('foo', 'bar')
+        assert_equal [['s2', 22.0], ['s3', 33.0]], r.zinter('foo', 'bar', with_scores: true)
+
+        assert_equal %w[s2 s3], r.zinter('foo', 'bar', aggregate: :min)
+        assert_equal [['s2', 2.0], ['s3', 3.0]], r.zinter('foo', 'bar', aggregate: :min, with_scores: true)
+
+        assert_equal %w[s2 s3], r.zinter('foo', 'bar', aggregate: :max)
+        assert_equal [['s2', 20.0], ['s3', 30.0]], r.zinter('foo', 'bar', aggregate: :max, with_scores: true)
+      end
+    end
+
     def test_zinterstore
       r.zadd 'foo', 1, 's1'
       r.zadd 'bar', 2, 's1'
diff --git a/test/lint/streams.rb b/test/lint/streams.rb
index 6ac52c0e077385df767566fdff7f51d3d52a8716..d7060fbb0e2c0e81b5066c30a11d3f420b6ae784 100644
--- a/test/lint/streams.rb
+++ b/test/lint/streams.rb
@@ -3,6 +3,7 @@
 module Lint
   module Streams
     MIN_REDIS_VERSION = '4.9.0'
+    MIN_REDIS_VERSION_XAUTOCLAIM = '6.2.0'
     ENTRY_ID_FORMAT = /\d+-\d+/.freeze
 
     def setup
@@ -116,10 +117,17 @@ module Lint
     end
 
     def test_xtrim_with_invalid_arguments
-      assert_equal 0, redis.xtrim('', '')
-      assert_equal 0, redis.xtrim(nil, nil)
-      assert_equal 0, redis.xtrim('s1', 0)
-      assert_equal 0, redis.xtrim('s1', -1, approximate: true)
+      if version >= '6.2'
+        assert_raises(Redis::CommandError) { redis.xtrim('', '') }
+        assert_raises(Redis::CommandError) { redis.xtrim(nil, nil) }
+        assert_equal 0, redis.xtrim('s1', 0)
+        assert_raises(Redis::CommandError) { redis.xtrim('s1', -1, approximate: true) }
+      else
+        assert_equal 0, redis.xtrim('', '')
+        assert_equal 0, redis.xtrim(nil, nil)
+        assert_equal 0, redis.xtrim('s1', 0)
+        assert_equal 0, redis.xtrim('s1', -1, approximate: true)
+      end
     end
 
     def test_xdel_with_splatted_entry_ids
@@ -483,6 +491,17 @@ module Lint
       assert_raises(Redis::CommandError) { redis.xreadgroup('g1', 'c1', 's1', %w[> >]) }
     end
 
+    def test_xreadgroup_a_trimmed_entry
+      redis.xgroup(:create, 'k1', 'g1', '0', mkstream: true)
+      entry_id = redis.xadd('k1', { value: 'v1' })
+
+      assert_equal({ 'k1' => [[entry_id, { 'value' => 'v1' }]] }, redis.xreadgroup('g1', 'c1', 'k1', '>'))
+      assert_equal({ 'k1' => [[entry_id, { 'value' => 'v1' }]] }, redis.xreadgroup('g1', 'c1', 'k1', '0'))
+      redis.xtrim('k1', 0)
+
+      assert_equal({ 'k1' => [[entry_id, nil]] }, redis.xreadgroup('g1', 'c1', 'k1', '0'))
+    end
+
     def test_xack_with_a_entry_id
       redis.xadd('s1', { f: 'v1' }, id: '0-1')
       redis.xgroup(:create, 's1', 'g1', '$')
@@ -626,6 +645,72 @@ module Lint
       assert_raises(Redis::CommandError) { redis.xclaim('', '', '', '', '') }
     end
 
+    def test_xautoclaim
+      omit_version(MIN_REDIS_VERSION_XAUTOCLAIM)
+
+      redis.xadd('s1', { f: 'v1' }, id: '0-1')
+      redis.xgroup(:create, 's1', 'g1', '$')
+      redis.xadd('s1', { f: 'v2' }, id: '0-2')
+      redis.xadd('s1', { f: 'v3' }, id: '0-3')
+      redis.xreadgroup('g1', 'c1', 's1', '>')
+      sleep 0.01
+
+      actual = redis.xautoclaim('s1', 'g1', 'c2', 10, '0-0')
+
+      assert_equal '0-0', actual['next']
+      assert_equal %w(0-2 0-3), actual['entries'].map(&:first)
+      assert_equal(%w(v2 v3), actual['entries'].map { |i| i.last['f'] })
+    end
+
+    def test_xautoclaim_with_justid_option
+      omit_version(MIN_REDIS_VERSION_XAUTOCLAIM)
+
+      redis.xadd('s1', { f: 'v1' }, id: '0-1')
+      redis.xgroup(:create, 's1', 'g1', '$')
+      redis.xadd('s1', { f: 'v2' }, id: '0-2')
+      redis.xadd('s1', { f: 'v3' }, id: '0-3')
+      redis.xreadgroup('g1', 'c1', 's1', '>')
+      sleep 0.01
+
+      actual = redis.xautoclaim('s1', 'g1', 'c2', 10, '0-0', justid: true)
+
+      assert_equal '0-0', actual['next']
+      assert_equal %w(0-2 0-3), actual['entries']
+    end
+
+    def test_xautoclaim_with_count_option
+      omit_version(MIN_REDIS_VERSION_XAUTOCLAIM)
+
+      redis.xadd('s1', { f: 'v1' }, id: '0-1')
+      redis.xgroup(:create, 's1', 'g1', '$')
+      redis.xadd('s1', { f: 'v2' }, id: '0-2')
+      redis.xadd('s1', { f: 'v3' }, id: '0-3')
+      redis.xreadgroup('g1', 'c1', 's1', '>')
+      sleep 0.01
+
+      actual = redis.xautoclaim('s1', 'g1', 'c2', 10, '0-0', count: 1)
+
+      assert_equal '0-3', actual['next']
+      assert_equal %w(0-2), actual['entries'].map(&:first)
+      assert_equal(%w(v2), actual['entries'].map { |i| i.last['f'] })
+    end
+
+    def test_xautoclaim_with_larger_interval
+      omit_version(MIN_REDIS_VERSION_XAUTOCLAIM)
+
+      redis.xadd('s1', { f: 'v1' }, id: '0-1')
+      redis.xgroup(:create, 's1', 'g1', '$')
+      redis.xadd('s1', { f: 'v2' }, id: '0-2')
+      redis.xadd('s1', { f: 'v3' }, id: '0-3')
+      redis.xreadgroup('g1', 'c1', 's1', '>')
+      sleep 0.01
+
+      actual = redis.xautoclaim('s1', 'g1', 'c2', 36_000, '0-0')
+
+      assert_equal '0-0', actual['next']
+      assert_equal [], actual['entries']
+    end
+
     def test_xpending
       redis.xadd('s1', { f: 'v1' }, id: '0-1')
       redis.xgroup(:create, 's1', 'g1', '$')
diff --git a/test/lint/strings.rb b/test/lint/strings.rb
index e9ecf58c53d7076b7933fa06d7fe74a549692aa3..78a483aa9424b85faffe5ae160781c740ea3cc84 100644
--- a/test/lint/strings.rb
+++ b/test/lint/strings.rb
@@ -51,6 +51,20 @@ module Lint
       end
     end
 
+    def test_set_with_exat
+      target_version "6.2" do
+        r.set("foo", "bar", exat: Time.now.to_i + 2)
+        assert_in_range 0..2, r.ttl("foo")
+      end
+    end
+
+    def test_set_with_pxat
+      target_version "6.2" do
+        r.set("foo", "bar", pxat: (1000 * Time.now.to_i) + 2000)
+        assert_in_range 0..2, r.ttl("foo")
+      end
+    end
+
     def test_set_with_nx
       target_version "2.6.12" do
         r.set("foo", "qux", nx: true)
@@ -83,6 +97,18 @@ module Lint
       end
     end
 
+    def test_set_with_get
+      target_version "6.2" do
+        r.set("foo", "qux")
+
+        assert_equal "qux", r.set("foo", "bar", get: true)
+        assert_equal "bar", r.get("foo")
+
+        assert_nil r.set("baz", "bar", get: true)
+        assert_equal "bar", r.get("baz")
+      end
+    end
+
     def test_setex
       assert r.setex("foo", 1, "bar")
       assert_equal "bar", r.get("foo")
@@ -115,6 +141,22 @@ module Lint
       end
     end
 
+    def test_getex
+      target_version "6.2" do
+        assert r.setex("foo", 1000, "bar")
+        assert_equal "bar", r.getex("foo", persist: true)
+        assert_equal(-1, r.ttl("foo"))
+      end
+    end
+
+    def test_getdel
+      target_version "6.2" do
+        assert r.set("foo", "bar")
+        assert_equal "bar", r.getdel("foo")
+        assert_nil r.get("foo")
+      end
+    end
+
     def test_getset
       r.set("foo", "bar")
 
diff --git a/test/lint/value_types.rb b/test/lint/value_types.rb
index c2ca2552f9866b17adffc14d6942abbcf8615d2f..e23715b7cfe8a7181965b0fb7881f8c8a96259bb 100644
--- a/test/lint/value_types.rb
+++ b/test/lint/value_types.rb
@@ -3,21 +3,21 @@
 module Lint
   module ValueTypes
     def test_exists
-      assert_equal false, r.exists("foo")
+      assert_equal 0, r.exists("foo")
 
       r.set("foo", "s1")
 
-      assert_equal true,  r.exists("foo")
+      assert_equal 1, r.exists("foo")
     end
 
     def test_exists_integer
       previous_exists_returns_integer = Redis.exists_returns_integer
-      Redis.exists_returns_integer = true
-      assert_equal 0, r.exists("foo")
+      Redis.exists_returns_integer = false
+      assert_equal false, r.exists("foo")
 
       r.set("foo", "s1")
 
-      assert_equal 1,  r.exists("foo")
+      assert_equal true, r.exists("foo")
     ensure
       Redis.exists_returns_integer = previous_exists_returns_integer
     end
diff --git a/test/scanning_test.rb b/test/scanning_test.rb
index e21d1bcf00248d1e74cfa6f072ce39cd4b9ce68a..ee51ec6ac0878b41cfc88d192e6451ae11619487 100644
--- a/test/scanning_test.rb
+++ b/test/scanning_test.rb
@@ -53,6 +53,25 @@ class TestScanning < Minitest::Test
     end
   end
 
+  def test_scan_type
+    target_version "6.0.0" do
+      r.debug :populate, 1000
+      r.zadd("foo", [1, "s1", 2, "s2", 3, "s3"])
+      r.zadd("bar", [6, "s1", 5, "s2", 4, "s3"])
+      r.hset("baz", "k1", "v1")
+
+      cursor = 0
+      all_keys = []
+      loop do
+        cursor, keys = r.scan cursor, type: "zset"
+        all_keys += keys
+        break if cursor == "0"
+      end
+
+      assert_equal 2, all_keys.uniq.size
+    end
+  end
+
   def test_scan_each_enumerator
     target_version "2.7.105" do
       r.debug :populate, 1000
@@ -78,6 +97,20 @@ class TestScanning < Minitest::Test
     end
   end
 
+  def test_scan_each_enumerator_type
+    target_version "6.0.0" do
+      r.debug :populate, 1000
+      r.zadd("key:zset", [1, "s1", 2, "s2", 3, "s3"])
+      r.hset("key:hash:1", "k1", "v1")
+      r.hset("key:hash:2", "k2", "v2")
+
+      keys_from_scan = r.scan_each(type: "hash").to_a.uniq
+      all_keys = r.keys "key:hash:*"
+
+      assert all_keys.sort == keys_from_scan.sort
+    end
+  end
+
   def test_scan_each_block
     target_version "2.7.105" do
       r.debug :populate, 100
diff --git a/test/sentinel_test.rb b/test/sentinel_test.rb
index 5122235af11d3d156c0dc9897e8b788afa96a5d4..d913feed1e159a5ae7e777c1e58e6fe703f8e540 100644
--- a/test/sentinel_test.rb
+++ b/test/sentinel_test.rb
@@ -257,6 +257,49 @@ class SentinelTest < Minitest::Test
     assert_equal [%w[auth bar], %w[role]], commands[:m1]
   end
 
+  def test_authentication_with_acl
+    commands = { s1: [], m1: [] }
+
+    sentinel = lambda do |port|
+      {
+        auth: lambda do |user, pass|
+          commands[:s1] << ['auth', user, pass]
+          '+OK'
+        end,
+        select: lambda do |db|
+          commands[:s1] << ['select', db]
+          '-ERR unknown command `select`'
+        end,
+        sentinel: lambda do |command, *args|
+          commands[:s1] << [command, *args]
+          ['127.0.0.1', port.to_s]
+        end
+      }
+    end
+
+    master = {
+      auth: lambda do |user, pass|
+        commands[:m1] << ['auth', user, pass]
+        '+OK'
+      end,
+      role: lambda do
+        commands[:m1] << ['role']
+        ['master']
+      end
+    }
+
+    RedisMock.start(master) do |master_port|
+      RedisMock.start(sentinel.call(master_port)) do |sen_port|
+        s = [{ host: '127.0.0.1', port: sen_port, username: 'bob', password: 'foo' }]
+        r = Redis.new(host: 'master1', sentinels: s, role: :master, username: 'alice', password: 'bar')
+        assert r.ping
+      end
+    end
+
+    assert_equal [%w[auth bob foo], %w[get-master-addr-by-name master1]], commands[:s1]
+    assert_equal [%w[auth alice bar], %w[role]], commands[:m1]
+  end
+
   def test_sentinel_role_mismatch
     sentinels = [{ host: "127.0.0.1", port: 26_381 }]
 
diff --git a/test/transactions_test.rb b/test/transactions_test.rb
index fd76af634de5e300ce2620bc9bc404295697e360..d86c6c1dfdd994807361d6aa62c647e634de86cc 100644
--- a/test/transactions_test.rb
+++ b/test/transactions_test.rb
@@ -10,6 +10,8 @@ class TestTransactions < Minitest::Test
 
     assert_equal "QUEUED", r.set("foo", "1")
     assert_equal "QUEUED", r.get("foo")
+    assert_equal "QUEUED", r.zincrby("bar", 1,  "baz") # Floatify
+    assert_equal "QUEUED", r.hsetnx("plop", "foo", "bar") # Boolify
 
     r.discard
 
diff --git a/test/url_param_test.rb b/test/url_param_test.rb
index 17ff9548215095a17fe6b44bd523336f3092c522..59626c295da416ba0280e8cdae4048a661c1b7bc 100644
--- a/test/url_param_test.rb
+++ b/test/url_param_test.rb
@@ -134,4 +134,15 @@ class TestUrlParam < Minitest::Test
 
     assert_equal "127.0.0.1", redis._client.host
   end
+
+  def test_user_and_password
+    redis = Redis.new(url: 'redis://johndoe:mysecret@foo.com:999/2')
+
+    assert_equal('redis', redis._client.scheme)
+    assert_equal('johndoe', redis._client.username)
+    assert_equal('mysecret', redis._client.password)
+    assert_equal('foo.com', redis._client.host)
+    assert_equal(999, redis._client.port)
+    assert_equal(2, redis._client.db)
+  end
 end