├── CODEOWNERS ├── .gitignore ├── .github └── workflows │ ├── issue-auto-assign.yml │ └── all.yml ├── catalog-info.yaml ├── register.go ├── examples ├── tls │ ├── README.md │ ├── docker │ │ ├── docker-compose.yml │ │ └── gen-test-certs.sh │ └── loadtest-tls.js └── loadtest.js ├── renovate.json ├── README.md ├── Makefile ├── redis ├── tls_test.go ├── module.go ├── options.go ├── stub_test.go ├── client.go └── client_test.go ├── go.mod ├── LICENSE └── go.sum /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @grafana/k6-core 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /k6 2 | 3 | # we use the config from the main k6's repository 4 | # https://github.com/grafana/k6/blob/master/.golangci.yml 5 | .golangci.yml -------------------------------------------------------------------------------- /.github/workflows/issue-auto-assign.yml: -------------------------------------------------------------------------------- 1 | name: "Auto assign maintainer to issue" 2 | on: 3 | issues: 4 | types: [opened] 5 | 6 | permissions: 7 | issues: write 8 | 9 | jobs: 10 | assign-maintainer: 11 | uses: grafana/k6/.github/workflows/issue-auto-assign.yml@master 12 | -------------------------------------------------------------------------------- /.github/workflows/all.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | # Enable manually triggering this workflow via the API or web UI 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - v* 10 | pull_request: 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | checks: 17 | uses: grafana/k6-ci/.github/workflows/all.yml@main 18 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: xk6-redis 5 | title: xk6-redis 6 | description: | 7 | A k6 extension to test the performance of a Redis instance. 8 | annotations: 9 | github.com/project-slug: grafana/xk6-redis 10 | spec: 11 | type: library 12 | owner: group:default/k6-core 13 | lifecycle: production 14 | -------------------------------------------------------------------------------- /register.go: -------------------------------------------------------------------------------- 1 | // Package redis only exists to register the redis extension 2 | package redis 3 | 4 | import ( 5 | "github.com/grafana/xk6-redis/redis" 6 | "go.k6.io/k6/js/modules" 7 | ) 8 | 9 | // Register the extension on module initialization, available to 10 | // import from JS as "k6/x/redis". 11 | func init() { 12 | modules.Register("k6/x/redis", new(redis.RootModule)) 13 | } 14 | -------------------------------------------------------------------------------- /examples/tls/README.md: -------------------------------------------------------------------------------- 1 | # How to run a k6 test against a Redis test server with TLS 2 | 3 | 1. Move in the docker folder `cd docker` 4 | 2. Run `sh gen-test-certs.sh` to generate custom TLS certificates that the docker container will use. 5 | 3. Run `docker-compose up` to start the Redis server with TLS enabled. 6 | 4. Connect to it with `redis-cli --tls --cert ./tests/tls/redis.crt --key ./tests/tls/redis.key --cacert ./tests/tls/ca.crt` and run `AUTH tjkbZ8jrwz3pGiku` to authenticate, and verify that the redis server is properly set up. 7 | 5. Build the k6 binary with `xk6 build --with github.com/k6io/xk6-redis=.` 8 | 5. Run `./k6 run loadtest-tls.js` to run the k6 load test with TLS enabled. -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["schedule:weekly"], 4 | "packageRules": [ 5 | { 6 | "description": "Disable updates for all dependencies by default", 7 | "matchManagers": ["gomod"], 8 | "enabled": false 9 | }, 10 | { 11 | "description": "k6 core and ecosystem packages", 12 | "matchManagers": ["gomod"], 13 | "matchPackageNames": ["go.k6.io/k6"], 14 | "groupName": "k6 ecosystem", 15 | "enabled": true 16 | }, 17 | { 18 | "description": "Redis library - critical for functionality", 19 | "matchManagers": ["gomod"], 20 | "matchPackageNames": ["github.com/redis/go-redis/**"], 21 | "enabled": true 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /examples/tls/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | redis: 5 | image: docker.io/bitnami/redis:7.0.8 6 | user: root 7 | restart: always 8 | environment: 9 | - ALLOW_EMPTY_PASSWORD=false 10 | - REDIS_PASSWORD=tjkbZ8jrwz3pGiku 11 | - REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL 12 | - REDIS_EXTRA_FLAGS=--loglevel verbose --tls-auth-clients optional 13 | - REDIS_TLS_ENABLED=yes 14 | - REDIS_TLS_PORT=6379 15 | - REDIS_TLS_CERT_FILE=/tls/redis.crt 16 | - REDIS_TLS_KEY_FILE=/tls/redis.key 17 | - REDIS_TLS_CA_FILE=/tls/ca.crt 18 | ports: 19 | - "6379:6379" 20 | volumes: 21 | - redis_data:/bitnami/redis/data 22 | - ./tests/tls:/tls 23 | 24 | volumes: 25 | redis_data: 26 | driver: local 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xk6-redis 2 | 3 | This is an official [k6](https://github.com/grafana/k6) extension, it's a client library for [Redis](https://redis.io) database. Check out the extension's documentation [here](https://grafana.com/docs/k6/latest/javascript-api/k6-experimental/redis). 4 | 5 | ## Get started 6 | 7 | Add the extension's import 8 | ```js 9 | import redis from "k6/x/redis"; 10 | ``` 11 | 12 | Define the client 13 | ```js 14 | import redis from "k6/x/redis"; 15 | 16 | // Instantiate a new Redis client using a URL. 17 | // The connection will be established on the first command call. 18 | const client = new redis.Client('redis://localhost:6379'); 19 | ``` 20 | 21 | Call the API from the VU context 22 | 23 | ```js 24 | export default function () { 25 | redisClient.sadd("crocodile_ids", ...crocodileIDs); 26 | ... 27 | } 28 | ``` 29 | 30 | ## Build 31 | 32 | The most common and simple case is to use k6 with automatic extension resolution. Simply add the extension's import and k6 will resolve the dependency automtically. 33 | However, if you prefer to build it from source using xk6, first ensure you have the prerequisites: 34 | 35 | - [Go toolchain](https://go101.org/article/go-toolchain.html) 36 | - Git 37 | 38 | Then: 39 | 40 | 1. Install `xk6`: 41 | ```shell 42 | go install go.k6.io/xk6/cmd/xk6@latest 43 | ``` 44 | 45 | 2. Build the binary: 46 | ```shell 47 | xk6 build --with github.com/grafana/xk6-redis 48 | ``` 49 | -------------------------------------------------------------------------------- /examples/tls/loadtest-tls.js: -------------------------------------------------------------------------------- 1 | import redis from "k6/x/redis"; 2 | import exec from "k6/execution"; 3 | 4 | export const options = { 5 | vus: 10, 6 | iterations: 200, 7 | insecureSkipTLSVerify: true, 8 | }; 9 | 10 | // Instantiate a new Redis client using a URL 11 | // const client = new redis.Client('rediss://localhost:6379') 12 | const client = new redis.Client({ 13 | password: "tjkbZ8jrwz3pGiku", 14 | socket:{ 15 | host: "localhost", 16 | port: 6379, 17 | tls: { 18 | ca: [open('docker/tests/tls/ca.crt')], 19 | cert: open('docker/tests/tls/client.crt'), // client cert 20 | key: open('docker/tests/tls/client.key'), // client private key 21 | } 22 | } 23 | }); 24 | 25 | export default async function () { 26 | // VUs are executed in a parallel fashion, 27 | // thus, to ensure that parallel VUs are not 28 | // modifying the same key at the same time, 29 | // we use keys indexed by the VU id. 30 | const key = `foo-${exec.vu.idInTest}`; 31 | 32 | await client.set(`foo-${exec.vu.idInTest}`, 1) 33 | 34 | let value = await client.get(`foo-${exec.vu.idInTest}`) 35 | value = await client.incrBy(`foo-${exec.vu.idInTest}`, value) 36 | 37 | await client.del(`foo-${exec.vu.idInTest}`) 38 | const exists = await client.exists(`foo-${exec.vu.idInTest}`) 39 | if (exists !== 0) { 40 | throw new Error("foo should have been deleted"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS += --silent 2 | GOLANGCI_CONFIG ?= .golangci.yml 3 | 4 | all: clean lint test build 5 | 6 | ## help: Prints a list of available build targets. 7 | help: 8 | echo "Usage: make ... " 9 | echo "" 10 | echo "Available targets are:" 11 | echo '' 12 | sed -n 's/^##//p' ${PWD}/Makefile | column -t -s ':' | sed -e 's/^/ /' 13 | echo 14 | echo "Targets run by default are: `sed -n 's/^all: //p' ./Makefile | sed -e 's/ /, /g' | sed -e 's/\(.*\), /\1, and /'`" 15 | 16 | ## build: Builds a custom 'k6' with the local extension. 17 | build: 18 | xk6 build --with $(shell go list -m)=. 19 | 20 | ## linter-config: Checks if the linter config exists, if not, downloads it from the main k6 repository. 21 | linter-config: 22 | test -s "${GOLANGCI_CONFIG}" || (echo "No linter config, downloading from main k6 repository..." && curl --silent --show-error --fail --no-location https://raw.githubusercontent.com/grafana/k6/master/.golangci.yml --output "${GOLANGCI_CONFIG}") 23 | 24 | ## check-linter-version: Checks if the linter version is the same as the one specified in the linter config. 25 | check-linter-version: 26 | (golangci-lint version | grep "version $(shell head -n 1 .golangci.yml | tr -d '\# ')") || echo "Your installation of golangci-lint is different from the one that is specified in k6's linter config (there it's $(shell head -n 1 .golangci.yml | tr -d '\# ')). Results could be different in the CI." 27 | 28 | ## test: Executes any tests. 29 | test: 30 | go test -race -timeout 30s ./... 31 | 32 | ## lint: Runs the linters. 33 | lint: linter-config check-linter-version 34 | echo "Running linters..." 35 | golangci-lint run --out-format=tab ./... 36 | 37 | ## check: Runs the linters and tests. 38 | check: lint test 39 | 40 | ## clean: Removes any previously created artifacts/downloads. 41 | clean: 42 | echo "Cleaning up..." 43 | rm -f ./k6 44 | rm .golangci.yml 45 | 46 | .PHONY: test lint check build clean linter-config check-linter-version -------------------------------------------------------------------------------- /examples/tls/docker/gen-test-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Generate some test certificates which are used by the regression test suite: 4 | # 5 | # tests/tls/ca.{crt,key} Self signed CA certificate. 6 | # tests/tls/redis.{crt,key} A certificate with no key usage/policy restrictions. 7 | # tests/tls/client.{crt,key} A certificate restricted for SSL client usage. 8 | # tests/tls/server.{crt,key} A certificate restricted for SSL server usage. 9 | # tests/tls/redis.dh DH Params file. 10 | 11 | generate_cert() { 12 | local name=$1 13 | local cn="$2" 14 | local opts="$3" 15 | 16 | local keyfile=tests/tls/${name}.key 17 | local certfile=tests/tls/${name}.crt 18 | 19 | [ -f $keyfile ] || openssl genrsa -out $keyfile 2048 20 | openssl req \ 21 | -new -sha256 \ 22 | -subj "/O=Redis Test/CN=$cn" \ 23 | -key $keyfile | 24 | openssl x509 \ 25 | -req -sha256 \ 26 | -CA tests/tls/ca.crt \ 27 | -CAkey tests/tls/ca.key \ 28 | -CAserial tests/tls/ca.txt \ 29 | -CAcreateserial \ 30 | -days 365 \ 31 | $opts \ 32 | -out $certfile 33 | } 34 | 35 | mkdir -p tests/tls 36 | [ -f tests/tls/ca.key ] || openssl genrsa -out tests/tls/ca.key 4096 37 | openssl req \ 38 | -x509 -new -nodes -sha256 \ 39 | -key tests/tls/ca.key \ 40 | -days 3650 \ 41 | -subj '/O=Redis Test/CN=Certificate Authority' \ 42 | -out tests/tls/ca.crt 43 | 44 | cat >tests/tls/openssl.cnf <<_END_ 45 | [ server_cert ] 46 | keyUsage = digitalSignature, keyEncipherment 47 | nsCertType = server 48 | 49 | [ client_cert ] 50 | keyUsage = digitalSignature, keyEncipherment 51 | nsCertType = client 52 | _END_ 53 | 54 | generate_cert server "Server-only" "-extfile tests/tls/openssl.cnf -extensions server_cert" 55 | generate_cert client "Client-only" "-extfile tests/tls/openssl.cnf -extensions client_cert" 56 | generate_cert redis "Generic-cert" 57 | 58 | [ -f tests/tls/redis.dh ] || openssl dhparam -out tests/tls/redis.dh 2048 59 | -------------------------------------------------------------------------------- /redis/tls_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/pem" 10 | "errors" 11 | "fmt" 12 | "math/big" 13 | "time" 14 | ) 15 | 16 | // Generate self-signed TLS certificate and private key for testing purposes, 17 | // and return them as PEM encoded data. 18 | // Source: https://eli.thegreenplace.net/2021/go-https-servers-with-tls/ 19 | func generateTLSCert() (certPEM, privateKeyPEM []byte, err error) { 20 | pkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 21 | if err != nil { 22 | return nil, nil, fmt.Errorf("failed to generate private key: %w", err) 23 | } 24 | 25 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 26 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 27 | if err != nil { 28 | return nil, nil, fmt.Errorf("failed to generate serial number: %w", err) 29 | } 30 | 31 | template := x509.Certificate{ 32 | SerialNumber: serialNumber, 33 | Subject: pkix.Name{ 34 | Organization: []string{"Grafana Labs"}, 35 | }, 36 | DNSNames: []string{"localhost"}, 37 | NotBefore: time.Now(), 38 | NotAfter: time.Now().Add(3 * time.Hour), 39 | KeyUsage: x509.KeyUsageDigitalSignature, 40 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, 41 | BasicConstraintsValid: true, 42 | } 43 | 44 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &pkey.PublicKey, pkey) 45 | if err != nil { 46 | return nil, nil, fmt.Errorf("failed to create certificate: %w", err) 47 | } 48 | 49 | certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 50 | if certPEM == nil { 51 | return nil, nil, errors.New("failed to encode certificate to PEM") 52 | } 53 | 54 | privBytes, err := x509.MarshalPKCS8PrivateKey(pkey) 55 | if err != nil { 56 | return nil, nil, fmt.Errorf("failed to marshal private key: %w", err) 57 | } 58 | privateKeyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) 59 | if privateKeyPEM == nil { 60 | return nil, nil, errors.New("failed to encode private key to PEM") 61 | } 62 | 63 | return certPEM, privateKeyPEM, nil 64 | } 65 | -------------------------------------------------------------------------------- /redis/module.go: -------------------------------------------------------------------------------- 1 | // Package redis implements a redis client for k6. 2 | package redis 3 | 4 | import ( 5 | "errors" 6 | 7 | "github.com/grafana/sobek" 8 | "go.k6.io/k6/js/common" 9 | "go.k6.io/k6/js/modules" 10 | ) 11 | 12 | type ( 13 | // RootModule is the global module instance that will create Client 14 | // instances for each VU. 15 | RootModule struct{} 16 | 17 | // ModuleInstance represents an instance of the JS module. 18 | ModuleInstance struct { 19 | vu modules.VU 20 | 21 | *Client 22 | } 23 | ) 24 | 25 | // Ensure the interfaces are implemented correctly 26 | var ( 27 | _ modules.Instance = &ModuleInstance{} 28 | _ modules.Module = &RootModule{} 29 | ) 30 | 31 | // New returns a pointer to a new RootModule instance 32 | func New() *RootModule { 33 | return &RootModule{} 34 | } 35 | 36 | // NewModuleInstance implements the modules.Module interface and returns 37 | // a new instance for each VU. 38 | func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance { 39 | return &ModuleInstance{vu: vu, Client: &Client{vu: vu}} 40 | } 41 | 42 | // Exports implements the modules.Instance interface and returns 43 | // the exports of the JS module. 44 | func (mi *ModuleInstance) Exports() modules.Exports { 45 | return modules.Exports{Named: map[string]interface{}{ 46 | "Client": mi.NewClient, 47 | }} 48 | } 49 | 50 | // NewClient is the JS constructor for the redis Client. 51 | // 52 | // Under the hood, the redis.UniversalClient will be used. The universal client 53 | // supports failover/sentinel, cluster and single-node modes. Depending on the options, 54 | // the internal universal client instance will be one of those. 55 | // 56 | // The type of the underlying client depends on the following conditions: 57 | // If the first argument is a string, it's parsed as a Redis URL, and a 58 | // single-node Client is used. 59 | // Otherwise, an object is expected, and depending on its properties: 60 | // 1. If the masterName property is defined, a sentinel-backed FailoverClient is used. 61 | // 2. If the cluster property is defined, a ClusterClient is used. 62 | // 3. Otherwise, a single-node Client is used. 63 | // 64 | // To support being instantiated in the init context, while not 65 | // producing any IO, as it is the convention in k6, the produced 66 | // Client is initially configured, but in a disconnected state. 67 | // The connection is automatically established when using any of the Redis 68 | // commands exposed by the Client. 69 | func (mi *ModuleInstance) NewClient(call sobek.ConstructorCall) *sobek.Object { 70 | rt := mi.vu.Runtime() 71 | 72 | if len(call.Arguments) != 1 { 73 | common.Throw(rt, errors.New("must specify one argument")) 74 | } 75 | 76 | opts, err := readOptions(call.Arguments[0].Export()) 77 | if err != nil { 78 | common.Throw(rt, err) 79 | } 80 | 81 | client := &Client{ 82 | vu: mi.vu, 83 | redisOptions: opts, 84 | redisClient: nil, 85 | } 86 | 87 | return rt.ToValue(client).ToObject(rt) 88 | } 89 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafana/xk6-redis 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.10 6 | 7 | require ( 8 | github.com/grafana/sobek v0.0.0-20251124090928-9a028a30ff58 9 | github.com/redis/go-redis/v9 v9.17.2 10 | github.com/stretchr/testify v1.11.1 11 | go.k6.io/k6 v1.4.2 12 | 13 | // To facilitate the integration of the extension in the k6 core codebase 14 | // we need to use the same version of the dependencies as k6. 15 | gopkg.in/guregu/null.v3 v3.3.0 16 | ) 17 | 18 | require ( 19 | github.com/cenkalti/backoff/v5 v5.0.3 // indirect 20 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 23 | github.com/dlclark/regexp2 v1.11.5 // indirect 24 | github.com/evanw/esbuild v0.25.10 // indirect 25 | github.com/fatih/color v1.18.0 // indirect 26 | github.com/fsnotify/fsnotify v1.4.9 // indirect 27 | github.com/go-logr/logr v1.4.3 // indirect 28 | github.com/go-logr/stdr v1.2.2 // indirect 29 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect 30 | github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6 // indirect 31 | github.com/google/uuid v1.6.0 // indirect 32 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect 33 | github.com/josharian/intern v1.0.0 // indirect 34 | github.com/mailru/easyjson v0.9.0 // indirect 35 | github.com/mattn/go-colorable v0.1.14 // indirect 36 | github.com/mattn/go-isatty v0.0.20 // indirect 37 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd // indirect 38 | github.com/mstoykov/k6-taskqueue-lib v0.1.3 // indirect 39 | github.com/pmezard/go-difflib v1.0.0 // indirect 40 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect 41 | github.com/sirupsen/logrus v1.9.3 // indirect 42 | github.com/spf13/afero v1.1.2 // indirect 43 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 44 | go.opentelemetry.io/otel v1.38.0 // indirect 45 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect 46 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect 47 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect 48 | go.opentelemetry.io/otel/metric v1.38.0 // indirect 49 | go.opentelemetry.io/otel/sdk v1.38.0 // indirect 50 | go.opentelemetry.io/otel/trace v1.38.0 // indirect 51 | go.opentelemetry.io/proto/otlp v1.8.0 // indirect 52 | golang.org/x/crypto v0.45.0 // indirect 53 | golang.org/x/net v0.47.0 // indirect 54 | golang.org/x/sys v0.38.0 // indirect 55 | golang.org/x/text v0.31.0 // indirect 56 | golang.org/x/time v0.14.0 // indirect 57 | google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect 58 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect 59 | google.golang.org/grpc v1.75.0 // indirect 60 | google.golang.org/protobuf v1.36.10 // indirect 61 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 62 | gopkg.in/yaml.v3 v3.0.1 // indirect 63 | ) 64 | -------------------------------------------------------------------------------- /examples/loadtest.js: -------------------------------------------------------------------------------- 1 | import { check } from "k6"; 2 | import http from "k6/http"; 3 | import redis from "k6/x/redis"; 4 | import exec from "k6/execution"; 5 | import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.1/index.js"; 6 | 7 | export const options = { 8 | scenarios: { 9 | redisPerformance: { 10 | executor: "shared-iterations", 11 | vus: 10, 12 | iterations: 200, 13 | exec: "measureRedisPerformance", 14 | }, 15 | usingRedisData: { 16 | executor: "shared-iterations", 17 | vus: 10, 18 | iterations: 200, 19 | exec: "measureUsingRedisData", 20 | }, 21 | }, 22 | }; 23 | 24 | // Instantiate a new Redis client using a URL 25 | const redisClient = new redis.Client( 26 | // URL in the form of redis[s]://[[username][:password]@][host][:port][/db-number 27 | __ENV.REDIS_URL || "redis://localhost:6379", 28 | ); 29 | 30 | // Prepare an array of crocodile ids for later use 31 | // in the context of the measureUsingRedisData function. 32 | const crocodileIDs = new Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); 33 | 34 | export function measureRedisPerformance() { 35 | // VUs are executed in a parallel fashion, 36 | // thus, to ensure that parallel VUs are not 37 | // modifying the same key at the same time, 38 | // we use keys indexed by the VU id. 39 | const key = `foo-${exec.vu.idInTest}`; 40 | 41 | redisClient 42 | .set(`foo-${exec.vu.idInTest}`, 1) 43 | .then(() => redisClient.get(`foo-${exec.vu.idInTest}`)) 44 | .then((value) => redisClient.incrBy(`foo-${exec.vu.idInTest}`, value)) 45 | .then((_) => redisClient.del(`foo-${exec.vu.idInTest}`)) 46 | .then((_) => redisClient.exists(`foo-${exec.vu.idInTest}`)) 47 | .then((exists) => { 48 | if (exists !== 0) { 49 | throw new Error("foo should have been deleted"); 50 | } 51 | }); 52 | } 53 | 54 | export function setup() { 55 | redisClient.sadd("crocodile_ids", ...crocodileIDs); 56 | } 57 | 58 | export function measureUsingRedisData() { 59 | // Pick a random crocodile id from the dedicated redis set, 60 | // we have filled in setup(). 61 | redisClient 62 | .srandmember("crocodile_ids") 63 | .then((randomID) => { 64 | const url = `https://test-api.k6.io/public/crocodiles/${randomID}`; 65 | const res = http.get(url); 66 | 67 | check(res, { 68 | "status is 200": (r) => r.status === 200, 69 | "content-type is application/json": (r) => 70 | r.headers["content-type"] === "application/json", 71 | }); 72 | 73 | return url; 74 | }) 75 | .then((url) => redisClient.hincrby("k6_crocodile_fetched", url, 1)); 76 | } 77 | 78 | export function teardown() { 79 | redisClient.del("crocodile_ids"); 80 | } 81 | 82 | export function handleSummary(data) { 83 | redisClient 84 | .hgetall("k6_crocodile_fetched") 85 | .then((fetched) => Object.assign(data, { k6_crocodile_fetched: fetched })) 86 | .then((data) => 87 | redisClient.set(`k6_report_${Date.now()}`, JSON.stringify(data)) 88 | ) 89 | .then(() => redisClient.del("k6_crocodile_fetched")); 90 | 91 | return { 92 | stdout: textSummary(data, { indent: " ", enableColors: true }), 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /redis/options.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/redis/go-redis/v9" 13 | ) 14 | 15 | type singleNodeOptions struct { 16 | Socket *socketOptions `json:"socket,omitempty"` 17 | Username string `json:"username,omitempty"` 18 | Password string `json:"password,omitempty"` 19 | ClientName string `json:"clientName,omitempty"` 20 | Database int `json:"database,omitempty"` 21 | MaxRetries int `json:"maxRetries,omitempty"` 22 | MinRetryBackoff int64 `json:"minRetryBackoff,omitempty"` 23 | MaxRetryBackoff int64 `json:"maxRetryBackoff,omitempty"` 24 | } 25 | 26 | func (opts singleNodeOptions) toRedisOptions() (*redis.Options, error) { 27 | ropts := &redis.Options{} 28 | sopts := opts.Socket 29 | if err := setSocketOptions(ropts, sopts); err != nil { 30 | return nil, err 31 | } 32 | 33 | ropts.DB = opts.Database 34 | ropts.Username = opts.Username 35 | ropts.Password = opts.Password 36 | ropts.MaxRetries = opts.MaxRetries 37 | ropts.MinRetryBackoff = time.Duration(opts.MinRetryBackoff) 38 | ropts.MaxRetryBackoff = time.Duration(opts.MaxRetryBackoff) 39 | 40 | return ropts, nil 41 | } 42 | 43 | type socketOptions struct { 44 | Host string `json:"host,omitempty"` 45 | Port int `json:"port,omitempty"` 46 | TLS *tlsOptions `json:"tls,omitempty"` 47 | DialTimeout int64 `json:"dialTimeout,omitempty"` 48 | ReadTimeout int64 `json:"readTimeout,omitempty"` 49 | WriteTimeout int64 `json:"writeTimeout,omitempty"` 50 | PoolSize int `json:"poolSize,omitempty"` 51 | MinIdleConns int `json:"minIdleConns,omitempty"` 52 | MaxConnAge int64 `json:"maxConnAge,omitempty"` 53 | PoolTimeout int64 `json:"poolTimeout,omitempty"` 54 | IdleTimeout int64 `json:"idleTimeout,omitempty"` 55 | IdleCheckFrequency int64 `json:"idleCheckFrequency,omitempty"` 56 | } 57 | 58 | type tlsOptions struct { 59 | // TODO: Handle binary data (ArrayBuffer) for all these as well. 60 | CA []string `json:"ca,omitempty"` 61 | Cert string `json:"cert,omitempty"` 62 | Key string `json:"key,omitempty"` 63 | } 64 | 65 | type commonClusterOptions struct { 66 | MaxRedirects int `json:"maxRedirects,omitempty"` 67 | ReadOnly bool `json:"readOnly,omitempty"` 68 | RouteByLatency bool `json:"routeByLatency,omitempty"` 69 | RouteRandomly bool `json:"routeRandomly,omitempty"` 70 | } 71 | 72 | type clusterNodesMapOptions struct { 73 | commonClusterOptions 74 | Nodes []*singleNodeOptions `json:"nodes,omitempty"` 75 | } 76 | 77 | type clusterNodesStringOptions struct { 78 | commonClusterOptions 79 | Nodes []string `json:"nodes,omitempty"` 80 | } 81 | 82 | type sentinelOptions struct { 83 | singleNodeOptions 84 | MasterName string `json:"masterName,omitempty"` 85 | SentinelUsername string `json:"sentinelUsername,omitempty"` 86 | SentinelPassword string `json:"sentinelPassword,omitempty"` 87 | } 88 | 89 | // newOptionsFromObject validates and instantiates an options struct from its 90 | // map representation as exported from sobek.Runtime. 91 | func newOptionsFromObject(obj map[string]interface{}) (*redis.UniversalOptions, error) { 92 | var options interface{} 93 | if cluster, ok := obj["cluster"].(map[string]interface{}); ok { 94 | obj = cluster 95 | nodes, ok := cluster["nodes"].([]interface{}) 96 | if !ok { 97 | return nil, fmt.Errorf("cluster nodes property must be an array; got %T", cluster["nodes"]) 98 | } 99 | if len(nodes) == 0 { 100 | return nil, errors.New("cluster nodes property cannot be empty") 101 | } 102 | switch nodes[0].(type) { 103 | case map[string]interface{}: 104 | options = &clusterNodesMapOptions{} 105 | case string: 106 | options = &clusterNodesStringOptions{} 107 | default: 108 | return nil, fmt.Errorf("cluster nodes array must contain string or object elements; got %T", nodes[0]) 109 | } 110 | } else if _, ok := obj["masterName"]; ok { 111 | options = &sentinelOptions{} 112 | } else { 113 | options = &singleNodeOptions{} 114 | } 115 | 116 | jsonStr, err := json.Marshal(obj) 117 | if err != nil { 118 | return nil, fmt.Errorf("unable to serialize options to JSON %w", err) 119 | } 120 | 121 | // Instantiate a JSON decoder which will error on unknown 122 | // fields. As a result, if the input map contains an unknown 123 | // option, this function will produce an error. 124 | decoder := json.NewDecoder(bytes.NewReader(jsonStr)) 125 | decoder.DisallowUnknownFields() 126 | 127 | err = decoder.Decode(&options) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | return toUniversalOptions(options) 133 | } 134 | 135 | // newOptionsFromString parses the expected URL into redis.UniversalOptions. 136 | func newOptionsFromString(url string) (*redis.UniversalOptions, error) { 137 | opts, err := redis.ParseURL(url) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | return toUniversalOptions(opts) 143 | } 144 | 145 | func readOptions(options interface{}) (*redis.UniversalOptions, error) { 146 | var ( 147 | opts *redis.UniversalOptions 148 | err error 149 | ) 150 | switch val := options.(type) { 151 | case string: 152 | opts, err = newOptionsFromString(val) 153 | case map[string]interface{}: 154 | opts, err = newOptionsFromObject(val) 155 | default: 156 | return nil, fmt.Errorf("invalid options type: %T; expected string or object", val) 157 | } 158 | 159 | if err != nil { 160 | return nil, fmt.Errorf("invalid options; reason: %w", err) 161 | } 162 | 163 | return opts, nil 164 | } 165 | 166 | func toUniversalOptions(options interface{}) (*redis.UniversalOptions, error) { 167 | uopts := &redis.UniversalOptions{Protocol: 2} 168 | 169 | switch o := options.(type) { 170 | case *clusterNodesMapOptions: 171 | setClusterOptions(uopts, &o.commonClusterOptions) 172 | 173 | for _, n := range o.Nodes { 174 | ropts, err := n.toRedisOptions() 175 | if err != nil { 176 | return nil, err 177 | } 178 | if err = setConsistentOptions(uopts, ropts); err != nil { 179 | return nil, err 180 | } 181 | } 182 | case *clusterNodesStringOptions: 183 | setClusterOptions(uopts, &o.commonClusterOptions) 184 | 185 | for _, n := range o.Nodes { 186 | ropts, err := redis.ParseURL(n) 187 | if err != nil { 188 | return nil, err 189 | } 190 | if err = setConsistentOptions(uopts, ropts); err != nil { 191 | return nil, err 192 | } 193 | } 194 | case *sentinelOptions: 195 | uopts.MasterName = o.MasterName 196 | uopts.SentinelUsername = o.SentinelUsername 197 | uopts.SentinelPassword = o.SentinelPassword 198 | 199 | ropts, err := o.toRedisOptions() 200 | if err != nil { 201 | return nil, err 202 | } 203 | if err = setConsistentOptions(uopts, ropts); err != nil { 204 | return nil, err 205 | } 206 | case *singleNodeOptions: 207 | ropts, err := o.toRedisOptions() 208 | if err != nil { 209 | return nil, err 210 | } 211 | if err = setConsistentOptions(uopts, ropts); err != nil { 212 | return nil, err 213 | } 214 | case *redis.Options: 215 | if err := setConsistentOptions(uopts, o); err != nil { 216 | return nil, err 217 | } 218 | default: 219 | panic(fmt.Sprintf("unexpected options type %T", options)) 220 | } 221 | 222 | return uopts, nil 223 | } 224 | 225 | // Set UniversalOptions values from single-node options, ensuring that any 226 | // previously set values are consistent with the new values. This validates that 227 | // multiple node options set when using cluster mode are consistent with each other. 228 | // TODO: Break apart, simplify? 229 | // 230 | //nolint:gocognit,cyclop,funlen 231 | func setConsistentOptions(uopts *redis.UniversalOptions, opts *redis.Options) error { 232 | uopts.Addrs = append(uopts.Addrs, opts.Addr) 233 | 234 | // Only set the TLS config once. Note that this assumes the same config is 235 | // used in other single-node options, since doing the consistency check we 236 | // use for the other options would be tedious. 237 | if uopts.TLSConfig == nil && opts.TLSConfig != nil { 238 | uopts.TLSConfig = opts.TLSConfig 239 | } 240 | 241 | if uopts.DB != 0 && opts.DB != 0 && uopts.DB != opts.DB { 242 | return fmt.Errorf("inconsistent db option: %d != %d", uopts.DB, opts.DB) 243 | } 244 | uopts.DB = opts.DB 245 | 246 | if uopts.Username != "" && opts.Username != "" && uopts.Username != opts.Username { 247 | return fmt.Errorf("inconsistent username option: %s != %s", uopts.Username, opts.Username) 248 | } 249 | uopts.Username = opts.Username 250 | 251 | if uopts.Password != "" && opts.Password != "" && uopts.Password != opts.Password { 252 | return fmt.Errorf("inconsistent password option") 253 | } 254 | uopts.Password = opts.Password 255 | 256 | if uopts.ClientName != "" && opts.ClientName != "" && uopts.ClientName != opts.ClientName { 257 | return fmt.Errorf("inconsistent clientName option: %s != %s", uopts.ClientName, opts.ClientName) 258 | } 259 | uopts.ClientName = opts.ClientName 260 | 261 | if uopts.MaxRetries != 0 && opts.MaxRetries != 0 && uopts.MaxRetries != opts.MaxRetries { 262 | return fmt.Errorf("inconsistent maxRetries option: %d != %d", uopts.MaxRetries, opts.MaxRetries) 263 | } 264 | uopts.MaxRetries = opts.MaxRetries 265 | 266 | if uopts.MinRetryBackoff != 0 && opts.MinRetryBackoff != 0 && uopts.MinRetryBackoff != opts.MinRetryBackoff { 267 | return fmt.Errorf("inconsistent minRetryBackoff option: %d != %d", uopts.MinRetryBackoff, opts.MinRetryBackoff) 268 | } 269 | uopts.MinRetryBackoff = opts.MinRetryBackoff 270 | 271 | if uopts.MaxRetryBackoff != 0 && opts.MaxRetryBackoff != 0 && uopts.MaxRetryBackoff != opts.MaxRetryBackoff { 272 | return fmt.Errorf("inconsistent maxRetryBackoff option: %d != %d", uopts.MaxRetryBackoff, opts.MaxRetryBackoff) 273 | } 274 | uopts.MaxRetryBackoff = opts.MaxRetryBackoff 275 | 276 | if uopts.DialTimeout != 0 && opts.DialTimeout != 0 && uopts.DialTimeout != opts.DialTimeout { 277 | return fmt.Errorf("inconsistent dialTimeout option: %d != %d", uopts.DialTimeout, opts.DialTimeout) 278 | } 279 | uopts.DialTimeout = opts.DialTimeout 280 | 281 | if uopts.ReadTimeout != 0 && opts.ReadTimeout != 0 && uopts.ReadTimeout != opts.ReadTimeout { 282 | return fmt.Errorf("inconsistent readTimeout option: %d != %d", uopts.ReadTimeout, opts.ReadTimeout) 283 | } 284 | uopts.ReadTimeout = opts.ReadTimeout 285 | 286 | if uopts.WriteTimeout != 0 && opts.WriteTimeout != 0 && uopts.WriteTimeout != opts.WriteTimeout { 287 | return fmt.Errorf("inconsistent writeTimeout option: %d != %d", uopts.WriteTimeout, opts.WriteTimeout) 288 | } 289 | uopts.WriteTimeout = opts.WriteTimeout 290 | 291 | if uopts.PoolSize != 0 && opts.PoolSize != 0 && uopts.PoolSize != opts.PoolSize { 292 | return fmt.Errorf("inconsistent poolSize option: %d != %d", uopts.PoolSize, opts.PoolSize) 293 | } 294 | uopts.PoolSize = opts.PoolSize 295 | 296 | if uopts.MinIdleConns != 0 && opts.MinIdleConns != 0 && uopts.MinIdleConns != opts.MinIdleConns { 297 | return fmt.Errorf("inconsistent minIdleConns option: %d != %d", uopts.MinIdleConns, opts.MinIdleConns) 298 | } 299 | uopts.MinIdleConns = opts.MinIdleConns 300 | 301 | if uopts.ConnMaxLifetime != 0 && opts.ConnMaxLifetime != 0 && uopts.ConnMaxLifetime != opts.ConnMaxLifetime { 302 | return fmt.Errorf("inconsistent maxConnAge option: %d != %d", uopts.ConnMaxLifetime, opts.ConnMaxLifetime) 303 | } 304 | uopts.ConnMaxLifetime = opts.ConnMaxLifetime 305 | 306 | if uopts.PoolTimeout != 0 && opts.PoolTimeout != 0 && uopts.PoolTimeout != opts.PoolTimeout { 307 | return fmt.Errorf("inconsistent poolTimeout option: %d != %d", uopts.PoolTimeout, opts.PoolTimeout) 308 | } 309 | uopts.PoolTimeout = opts.PoolTimeout 310 | 311 | if uopts.ConnMaxIdleTime != 0 && opts.ConnMaxIdleTime != 0 && uopts.ConnMaxIdleTime != opts.ConnMaxIdleTime { 312 | return fmt.Errorf("inconsistent idleTimeout option: %d != %d", uopts.ConnMaxIdleTime, opts.ConnMaxIdleTime) 313 | } 314 | uopts.ConnMaxIdleTime = opts.ConnMaxIdleTime 315 | 316 | return nil 317 | } 318 | 319 | func setClusterOptions(uopts *redis.UniversalOptions, opts *commonClusterOptions) { 320 | uopts.MaxRedirects = opts.MaxRedirects 321 | uopts.ReadOnly = opts.ReadOnly 322 | uopts.RouteByLatency = opts.RouteByLatency 323 | uopts.RouteRandomly = opts.RouteRandomly 324 | } 325 | 326 | func setSocketOptions(opts *redis.Options, sopts *socketOptions) error { 327 | if sopts == nil { 328 | return fmt.Errorf("empty socket options") 329 | } 330 | opts.Addr = fmt.Sprintf("%s:%d", sopts.Host, sopts.Port) 331 | opts.DialTimeout = time.Duration(sopts.DialTimeout) * time.Millisecond 332 | opts.ReadTimeout = time.Duration(sopts.ReadTimeout) * time.Millisecond 333 | opts.WriteTimeout = time.Duration(sopts.WriteTimeout) * time.Millisecond 334 | opts.PoolSize = sopts.PoolSize 335 | opts.MinIdleConns = sopts.MinIdleConns 336 | opts.ConnMaxLifetime = time.Duration(sopts.MaxConnAge) * time.Millisecond 337 | opts.PoolTimeout = time.Duration(sopts.PoolTimeout) * time.Millisecond 338 | opts.ConnMaxIdleTime = time.Duration(sopts.IdleTimeout) * time.Millisecond 339 | 340 | if sopts.TLS != nil { 341 | //#nosec G402 342 | tlsCfg := &tls.Config{} 343 | if len(sopts.TLS.CA) > 0 { 344 | caCertPool := x509.NewCertPool() 345 | for _, cert := range sopts.TLS.CA { 346 | caCertPool.AppendCertsFromPEM([]byte(cert)) 347 | } 348 | tlsCfg.RootCAs = caCertPool 349 | } 350 | 351 | if sopts.TLS.Cert != "" && sopts.TLS.Key != "" { 352 | clientCertPair, err := tls.X509KeyPair([]byte(sopts.TLS.Cert), []byte(sopts.TLS.Key)) 353 | if err != nil { 354 | return err 355 | } 356 | tlsCfg.Certificates = []tls.Certificate{clientCertPair} 357 | } 358 | 359 | opts.TLSConfig = tlsCfg 360 | } 361 | 362 | return nil 363 | } 364 | -------------------------------------------------------------------------------- /redis/stub_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "slices" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | "testing" 16 | "unicode" 17 | ) 18 | 19 | // RunT starts a new redis stub TCP server for a given test context. 20 | // It registers the test cleanup after your test is done. 21 | func RunT(t testing.TB) *StubServer { 22 | s := NewStubServer() 23 | if err := s.Start(false, nil); err != nil { 24 | t.Fatalf("could not start RedisStub; reason: %s", err) 25 | } 26 | 27 | t.Cleanup(s.Close) 28 | 29 | return s 30 | } 31 | 32 | // RunTSecure starts a new redis stub TLS server for a given test context. 33 | // It registers the test cleanup after your test is done. 34 | // Optionally, a client certificate in PEM format can be passed to enable TLS 35 | // client authentication (mTLS). 36 | func RunTSecure(t testing.TB, clientCert []byte) *StubServer { 37 | s := NewStubServer() 38 | if err := s.Start(true, clientCert); err != nil { 39 | t.Fatalf("could not start RedisStub; reason: %s", err) 40 | } 41 | 42 | t.Cleanup(s.Close) 43 | 44 | return s 45 | } 46 | 47 | // StubServer is a stub server emulating a Redis server. 48 | // 49 | // It implements a minimal redis server capable of handling 50 | // redis request and response message in the standard RESP 51 | // protocol format. 52 | // 53 | // It is intended to be used in tests. It listens on a random 54 | // localhost port, and can be used to test client-server interactions. 55 | // It parses incoming requests, and calls the user-defined handlers 56 | // to simulate the server's behavior. 57 | // 58 | // It is not intended to be used in production. 59 | type StubServer struct { 60 | sync.Mutex 61 | waitGroup sync.WaitGroup 62 | 63 | listener net.Listener 64 | boundAddr *net.TCPAddr 65 | 66 | connections map[net.Conn]struct{} 67 | connectionCount int 68 | 69 | handlers map[string]func(*Connection, []string) 70 | processedCommands int 71 | commandsHistory [][]string 72 | 73 | tlsCert []byte 74 | cert *tls.Certificate 75 | 76 | // list of commands to not be recorded 77 | ignoredCommands []string 78 | } 79 | 80 | // NewStubServer instantiates a new RedisStub server. 81 | func NewStubServer() *StubServer { 82 | return &StubServer{ 83 | listener: nil, 84 | boundAddr: nil, 85 | connections: map[net.Conn]struct{}{}, 86 | handlers: make(map[string]func(*Connection, []string)), 87 | ignoredCommands: []string{"CLIENT"}, // this is usually not interesting 88 | } 89 | } 90 | 91 | // Start the RedisStub server. If secure is true, a TLS server with a 92 | // self-signed certificate will be started. Otherwise, an unencrypted TCP 93 | // server will start. 94 | // Optionally, a client certificate in PEM format can be passed to enable TLS 95 | // client authentication (mTLS). 96 | func (rs *StubServer) Start(secure bool, clientCert []byte) error { 97 | var ( 98 | addr = net.JoinHostPort("localhost", "0") 99 | listener net.Listener 100 | ) 101 | if secure { //nolint: nestif 102 | // TODO: Generate the cert only once per test run and reuse it, instead 103 | // of once per StubServer start? 104 | cert, pkey, err := generateTLSCert() 105 | if err != nil { 106 | return err 107 | } 108 | rs.tlsCert = cert 109 | certPair, err := tls.X509KeyPair(cert, pkey) 110 | if err != nil { 111 | return err 112 | } 113 | rs.cert = &certPair 114 | config := &tls.Config{ 115 | MinVersion: tls.VersionTLS13, 116 | PreferServerCipherSuites: true, 117 | Certificates: []tls.Certificate{certPair}, 118 | } 119 | if clientCert != nil { 120 | clientCertPool := x509.NewCertPool() 121 | clientCertPool.AppendCertsFromPEM(clientCert) 122 | config.ClientCAs = clientCertPool 123 | config.ClientAuth = tls.RequireAndVerifyClientCert 124 | } 125 | if listener, err = tls.Listen("tcp", addr, config); err != nil { 126 | return err 127 | } 128 | } else { 129 | var err error 130 | if listener, err = (&net.ListenConfig{}).Listen(context.Background(), "tcp", addr); err != nil { 131 | return err 132 | } 133 | } 134 | 135 | rs.listener = listener 136 | 137 | // the provided addr string binds to port zero, 138 | // which leads to automatic port selection by the OS. 139 | // We need to get the actual port the OS bound us to. 140 | boundAddr, ok := listener.Addr().(*net.TCPAddr) 141 | if !ok { 142 | return errors.New("could not get TCP address") 143 | } 144 | 145 | rs.boundAddr = boundAddr 146 | 147 | rs.waitGroup.Add(1) 148 | go func() { 149 | defer rs.waitGroup.Done() 150 | rs.listenAndServe(listener) 151 | 152 | rs.Lock() 153 | for c := range rs.connections { 154 | c.Close() //nolint:errcheck,gosec 155 | } 156 | rs.Unlock() 157 | }() 158 | 159 | // The redis-cli will always start a session by sending the 160 | // COMMAND message. 161 | rs.RegisterCommandHandler("COMMAND", func(c *Connection, _ []string) { 162 | c.WriteArray("OK") 163 | }) 164 | 165 | // We register a default PING command handler 166 | rs.RegisterCommandHandler("PING", func(c *Connection, args []string) { 167 | if len(args) == 1 { 168 | c.WriteBulkString(args[0]) 169 | } else { 170 | c.WriteSimpleString("PONG") 171 | } 172 | }) 173 | 174 | return nil 175 | } 176 | 177 | // Close stops the RedisStub server. 178 | func (rs *StubServer) Close() { 179 | rs.Lock() 180 | if rs.listener != nil { 181 | err := rs.listener.Close() 182 | if err != nil { 183 | // this is unrecoverable, so we panic. 184 | panic(err) 185 | } 186 | } 187 | rs.listener = nil 188 | rs.Unlock() 189 | 190 | rs.waitGroup.Wait() 191 | } 192 | 193 | // RegisterCommandHandler registers a handler for a redis command. 194 | // 195 | // The handler is called when the command is received. It gives access 196 | // to a `*Connection` object, which can be used to send responses, using 197 | // its `Write*` methods. 198 | func (rs *StubServer) RegisterCommandHandler(command string, handler func(*Connection, []string)) { 199 | rs.Lock() 200 | defer rs.Unlock() 201 | rs.handlers[strings.ToUpper(command)] = handler 202 | } 203 | 204 | // Addr returns the address of the RedisStub server. 205 | func (rs *StubServer) Addr() *net.TCPAddr { 206 | return rs.boundAddr 207 | } 208 | 209 | // HandledCommandsCount returns the total number of commands 210 | // ran since the redis stub server started. 211 | func (rs *StubServer) HandledCommandsCount() int { 212 | rs.Lock() 213 | defer rs.Unlock() 214 | return rs.processedCommands 215 | } 216 | 217 | // HandledConnectionsCount returns the number of established client 218 | // connections since the redis stub server started. 219 | func (rs *StubServer) HandledConnectionsCount() int { 220 | rs.Lock() 221 | defer rs.Unlock() 222 | return rs.connectionCount 223 | } 224 | 225 | // GotCommands returns the commands handled (ordered by arrival) by the redis server 226 | // since it started. 227 | func (rs *StubServer) GotCommands() [][]string { 228 | rs.Lock() 229 | defer rs.Unlock() 230 | return rs.commandsHistory 231 | } 232 | 233 | // TLSCertificate returns the TLS certificate used by the server. 234 | func (rs *StubServer) TLSCertificate() []byte { 235 | return rs.tlsCert 236 | } 237 | 238 | // listenAndServe listens on the redis server's listener, 239 | // and handles client connections. 240 | func (rs *StubServer) listenAndServe(l net.Listener) { 241 | for { 242 | nc, err := l.Accept() 243 | if err != nil { 244 | return 245 | } 246 | 247 | rs.waitGroup.Add(1) 248 | rs.Lock() 249 | rs.connections[nc] = struct{}{} 250 | rs.connectionCount++ 251 | rs.Unlock() 252 | 253 | go func() { 254 | defer rs.waitGroup.Done() 255 | defer nc.Close() //nolint:errcheck 256 | 257 | rs.handleConnection(nc) 258 | 259 | rs.Lock() 260 | delete(rs.connections, nc) 261 | rs.Unlock() 262 | }() 263 | } 264 | } 265 | 266 | // handleConnection handles a single redis client connection. 267 | func (rs *StubServer) handleConnection(nc net.Conn) { 268 | connection := NewConnection(bufio.NewReader(nc), bufio.NewWriter(nc)) 269 | 270 | for { 271 | command, args, err := connection.ParseRequest() 272 | if err != nil { 273 | connection.WriteError(ErrInvalidSyntax) 274 | return 275 | } 276 | 277 | if !slices.Contains(rs.ignoredCommands, command) { 278 | rs.Lock() 279 | request := append([]string{command}, args...) 280 | rs.commandsHistory = append(rs.commandsHistory, request) 281 | rs.Unlock() 282 | } 283 | 284 | rs.handleCommand(connection, command, args) 285 | connection.Flush() 286 | } 287 | } 288 | 289 | // ErrUnknownCommand is the error message returned when the server 290 | // is unable to handle the provided command (because it is not registered). 291 | var ErrUnknownCommand = errors.New("unknown command") 292 | 293 | // HandleCommand handles the provided command and arguments. 294 | // 295 | // If the command is not known, it writes the ErrUnknownCommand 296 | // error message to the Connection's writer. 297 | func (rs *StubServer) handleCommand(c *Connection, cmd string, args []string) { 298 | rs.Lock() 299 | handlerFn, ok := rs.handlers[cmd] 300 | rs.Unlock() 301 | if !ok { 302 | c.WriteError(ErrUnknownCommand) 303 | return 304 | } 305 | 306 | rs.Lock() 307 | rs.processedCommands++ 308 | rs.Unlock() 309 | 310 | handlerFn(c, args) 311 | } 312 | 313 | // Connection represents a client connection to the redis server. 314 | type Connection struct { 315 | writer *bufio.Writer 316 | reader *bufio.Reader 317 | mutex sync.Mutex 318 | } 319 | 320 | // NewConnection creates a new Connection from the provided 321 | // buffered reader and writer (usually a net.Conn instance). 322 | func NewConnection(r *bufio.Reader, w *bufio.Writer) *Connection { 323 | return &Connection{ 324 | reader: r, 325 | writer: w, 326 | } 327 | } 328 | 329 | // ParseRequest parses a request from the Connection's reader. 330 | // It returns the parsed command, arguments and any error. 331 | func (c *Connection) ParseRequest() (string, []string, error) { 332 | return NewRESPRequestReader(c.reader).ReadCommand() 333 | } 334 | 335 | // Flush flushes the Connection's writer, effectively sending 336 | // all buffered data to the client. 337 | func (c *Connection) Flush() { 338 | c.mutex.Lock() 339 | defer c.mutex.Unlock() 340 | if err := c.writer.Flush(); err != nil { 341 | // this is unrecoverable, so we panic. 342 | panic(err) 343 | } 344 | } 345 | 346 | // WriteSimpleString writes the provided value as a redis simple string message 347 | // to the Connection's writer. 348 | func (c *Connection) WriteSimpleString(s string) { 349 | c.callFn(func(w *RESPResponseWriter) { 350 | w.WriteSimpleString(s) 351 | }) 352 | } 353 | 354 | // WriteError writes the provided error as a redis error message 355 | // to the Connection's writer. 356 | func (c *Connection) WriteError(err error) { 357 | c.callFn(func(w *RESPResponseWriter) { 358 | w.WriteError(err) 359 | }) 360 | } 361 | 362 | // WriteInteger writes the provided integer as a redis integer message 363 | // to the Connection's writer. 364 | func (c *Connection) WriteInteger(i int) { 365 | c.callFn(func(w *RESPResponseWriter) { 366 | w.WriteInteger(i) 367 | }) 368 | } 369 | 370 | // WriteBulkString writes the provided string as a redis bulk string message 371 | // to the Connection's writer. 372 | func (c *Connection) WriteBulkString(s string) { 373 | c.callFn(func(w *RESPResponseWriter) { 374 | w.WriteBulkString(s) 375 | }) 376 | } 377 | 378 | // WriteArray writes the provided array of string as a redis array message 379 | // to the Connection's writer. 380 | func (c *Connection) WriteArray(arr ...string) { 381 | c.callFn(func(w *RESPResponseWriter) { 382 | w.WriteArray(arr...) 383 | }) 384 | } 385 | 386 | // WriteNull writes a redis Null message to the Connection's writer. 387 | func (c *Connection) WriteNull() { 388 | c.callFn(func(w *RESPResponseWriter) { 389 | w.WriteNull() 390 | }) 391 | } 392 | 393 | // WriteOK is a helper method for writing the OK response to the 394 | // Connection's writer. 395 | func (c *Connection) WriteOK() { 396 | c.callFn(func(w *RESPResponseWriter) { 397 | w.WriteSimpleString("OK") 398 | }) 399 | } 400 | 401 | // callFn calls the provided function in a locking manner. 402 | // 403 | // It is used to ensure that the Connection's writer is not 404 | // modified while it is being written to. 405 | func (c *Connection) callFn(fn func(*RESPResponseWriter)) { 406 | c.mutex.Lock() 407 | defer c.mutex.Unlock() 408 | fn(&RESPResponseWriter{c.writer}) 409 | } 410 | 411 | // RESPRequestReader is a RESP protocol request reader. 412 | type RESPRequestReader struct { 413 | reader *bufio.Reader 414 | } 415 | 416 | // NewRESPRequestReader returns a new RESPRequestReader. 417 | func NewRESPRequestReader(reader *bufio.Reader) *RESPRequestReader { 418 | return &RESPRequestReader{reader: reader} 419 | } 420 | 421 | // ReadCommand reads a RESP command from the reader, parses it, and 422 | // returns the parsed command, args, and any potential error encountered. 423 | func (rrr *RESPRequestReader) ReadCommand() (string, []string, error) { 424 | elements, err := scanArray(rrr.reader) 425 | if err != nil { 426 | return "", nil, err 427 | } 428 | 429 | if len(elements) < 1 { 430 | return "", nil, ErrInvalidSyntax 431 | } 432 | 433 | return strings.ToUpper(elements[0]), elements[1:], nil 434 | } 435 | 436 | // ErrInvalidSyntax is returned when a RESP protocol message 437 | // is malformed and cannot be parsed. 438 | var ErrInvalidSyntax = errors.New("invalid RESP protocol syntax") 439 | 440 | // Prefix is a placeholder type for the prefix symbol of 441 | // RESP response message. 442 | type Prefix byte 443 | 444 | // RESP protocol response type prefixes definitions 445 | const ( 446 | SimpleStringPrefix Prefix = '+' 447 | ErrorPrefix = '-' 448 | IntegerPrefix = ':' 449 | BulkStringPrefix = '$' 450 | ArrayPrefix = '*' 451 | UnknownPrefix 452 | ) 453 | 454 | // RESPResponseWriter is a RESP protocol response writer. 455 | type RESPResponseWriter struct { 456 | writer *bufio.Writer 457 | } 458 | 459 | // WriteSimpleString writes a redis inline string 460 | func (rw *RESPResponseWriter) WriteSimpleString(s string) { 461 | _, _ = fmt.Fprintf(rw.writer, "+%s\r\n", inline(s)) 462 | } 463 | 464 | // WriteError writes a redis 'Error' 465 | func (rw *RESPResponseWriter) WriteError(err error) { 466 | _, _ = fmt.Fprintf(rw.writer, "-%s\r\n", inline(err.Error())) 467 | } 468 | 469 | // WriteInteger writes an integer 470 | func (rw *RESPResponseWriter) WriteInteger(n int) { 471 | _, _ = fmt.Fprintf(rw.writer, ":%d\r\n", n) 472 | } 473 | 474 | // WriteBulkString writes a bulk string 475 | func (rw *RESPResponseWriter) WriteBulkString(s string) { 476 | _, _ = fmt.Fprintf(rw.writer, "$%d\r\n%s\r\n", len(s), s) 477 | } 478 | 479 | // WriteArray writes a list of strings (bulk) 480 | func (rw *RESPResponseWriter) WriteArray(strs ...string) { 481 | rw.writeLen(len(strs)) 482 | for _, s := range strs { 483 | if s == "" || s == "nil" { 484 | rw.WriteNull() 485 | continue 486 | } 487 | 488 | rw.WriteBulkString(s) 489 | } 490 | } 491 | 492 | // WriteNull writes a redis Null element 493 | func (rw *RESPResponseWriter) WriteNull() { 494 | _, _ = fmt.Fprintf(rw.writer, "$-1\r\n") 495 | } 496 | 497 | func (rw *RESPResponseWriter) writeLen(n int) { 498 | _, _ = fmt.Fprintf(rw.writer, "*%d\r\n", n) 499 | } 500 | 501 | func inline(s string) string { 502 | return strings.Map(func(r rune) rune { 503 | if unicode.IsSpace(r) { 504 | return ' ' 505 | } 506 | return r 507 | }, s) 508 | } 509 | 510 | // scanBulkString reads a RESP bulk string message from a bufio.reader 511 | // 512 | // It also strips it from its prefix and trailing CRLF character, returning 513 | // only the interpretable content of the message. 514 | func scanBulkString(r *bufio.Reader) (string, error) { 515 | line, err := scanLine(r) 516 | if err != nil { 517 | return "", err 518 | } 519 | 520 | switch Prefix(line[0]) { 521 | case BulkStringPrefix: 522 | length, err := strconv.Atoi(line[1 : len(line)-2]) 523 | if err != nil { 524 | return "", err 525 | } 526 | 527 | if length < 0 { 528 | return line, nil 529 | } 530 | 531 | buf := make([]byte, length+2) 532 | for pos := 0; pos < length+2; { 533 | n, err := r.Read(buf[pos:]) 534 | if err != nil { 535 | return "", err 536 | } 537 | 538 | pos += n 539 | } 540 | 541 | return string(buf[:len(buf)-2]), nil 542 | default: 543 | return "", ErrInvalidSyntax 544 | } 545 | } 546 | 547 | // scanArray reads a RESP array message from a bufio.Reader. 548 | // 549 | // It strips it from its prefix and trailing CRLF character, 550 | // returning only the interpretable content of the message. 551 | func scanArray(r *bufio.Reader) ([]string, error) { 552 | line, err := scanLine(r) 553 | if err != nil { 554 | return nil, err 555 | } 556 | 557 | if len(line) < 3 { 558 | return nil, ErrInvalidSyntax 559 | } 560 | 561 | if Prefix(line[0]) != ArrayPrefix { 562 | return nil, ErrInvalidSyntax 563 | } 564 | 565 | length, err := strconv.Atoi(line[1 : len(line)-2]) 566 | if err != nil { 567 | return nil, err 568 | } 569 | 570 | var elements []string 571 | for ; length > 0; length-- { 572 | next, err := scanBulkString(r) 573 | if err != nil { 574 | return nil, err 575 | } 576 | 577 | elements = append(elements, next) 578 | } 579 | 580 | return elements, nil 581 | } 582 | 583 | // scanLine reads a RESP protocol line from a bufio.Reader. 584 | func scanLine(r *bufio.Reader) (string, error) { 585 | line, err := r.ReadString('\n') 586 | if err != nil { 587 | return "", err 588 | } 589 | 590 | if len(line) < 3 { 591 | return "", ErrInvalidSyntax 592 | } 593 | 594 | return line, nil 595 | } 596 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | buf.build/gen/go/gogo/protobuf/protocolbuffers/go v1.36.10-20240617172848-e1dbca2775a7.1 h1:A0G7t6KDoDJ7GuAU+ALdp8fCfTlx87ImTx69fXYN3X8= 2 | buf.build/gen/go/gogo/protobuf/protocolbuffers/go v1.36.10-20240617172848-e1dbca2775a7.1/go.mod h1:3ddKE6u98YQFS1jpuYmVEmU1fdAiHqB5Re6S3E16/mI= 3 | buf.build/gen/go/prometheus/prometheus/protocolbuffers/go v1.36.10-20251006115534-cbd485bd5afd.1 h1:nhEyqT9cIY8IpBTJTmIV2Sfn90YLzSSHXmX0hmLVTtk= 4 | buf.build/gen/go/prometheus/prometheus/protocolbuffers/go v1.36.10-20251006115534-cbd485bd5afd.1/go.mod h1:BdURQlk1lXab5ov60A7yLZZONSP0Cho+RkOntf+FZF8= 5 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= 6 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 7 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 8 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 9 | github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= 10 | github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= 11 | github.com/Soontao/goHttpDigestClient v0.0.0-20170320082612-6d28bb1415c5 h1:k+1+doEm31k0rRjCjLnGG3YRkuO9ljaEyS2ajZd6GK8= 12 | github.com/Soontao/goHttpDigestClient v0.0.0-20170320082612-6d28bb1415c5/go.mod h1:5Q4+CyR7+Q3VMG8f78ou+QSX/BNUNUx5W48eFRat8DQ= 13 | github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= 14 | github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= 15 | github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= 16 | github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= 17 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 18 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 19 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 20 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 21 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 22 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 23 | github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= 24 | github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= 25 | github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 26 | github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 27 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 28 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 29 | github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU= 30 | github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= 31 | github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= 32 | github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= 33 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 34 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 35 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 37 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 38 | github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 39 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 40 | github.com/evanw/esbuild v0.25.10 h1:8cl6FntLWO4AbqXWqMWgYrvdm8lLSFm5HjU/HY2N27E= 41 | github.com/evanw/esbuild v0.25.10/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= 42 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 43 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 44 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 45 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 46 | github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w= 47 | github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= 48 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 49 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 50 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 51 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 52 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 53 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= 54 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 55 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 56 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 57 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 58 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 59 | github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6 h1:ZgoomqkdjGbQ3+qQXCkvYMCDvGDNg2k5JJDjjdTB6jY= 60 | github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= 61 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 62 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 63 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 64 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 65 | github.com/grafana/k6build v0.5.15 h1:4I5dkAWSMvXsElS1OpLbHj6ZXnebXZGnmwDXy5vcwSQ= 66 | github.com/grafana/k6build v0.5.15/go.mod h1:Sk7SUiCnx2AgkirG3PrCtmJKYL+a8EeRdTzFfbxP0X8= 67 | github.com/grafana/k6provider v0.2.0 h1:Zu8FBnk6cJyTTkpCA+y+Ravc2YFeAQjsIfPpcbZtfB0= 68 | github.com/grafana/k6provider v0.2.0/go.mod h1:TJ6vzPm4yDQ2ji/Fet0dFOpdjktrKZp4hsnLZhRoZVA= 69 | github.com/grafana/sobek v0.0.0-20251124090928-9a028a30ff58 h1:Mk4SXbmvIW1zMUzyWDMEd5bD/wXe9bq27aGD4TdxevU= 70 | github.com/grafana/sobek v0.0.0-20251124090928-9a028a30ff58/go.mod h1:YtuqiJX1W3XvRSilL/kUZzduJG3phPJWyzM9DiIEfBo= 71 | github.com/grafana/xk6-dashboard v0.7.13 h1:jYD0zbxrYgz3hckRgoZ8nbd6AXYZhFvJk4WCdcMVvFw= 72 | github.com/grafana/xk6-dashboard v0.7.13/go.mod h1:D+k5+Nf836MHpELDqGxUs9eCRAOYE+d5HYM9zDwdILw= 73 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= 74 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= 75 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= 76 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= 77 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 78 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 79 | github.com/influxdata/influxdb1-client v0.0.0-20190402204710-8ff2fc3824fc h1:KpMgaYJRieDkHZJWY3LMafvtqS/U8xX6+lUN+OKpl/Y= 80 | github.com/influxdata/influxdb1-client v0.0.0-20190402204710-8ff2fc3824fc/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= 81 | github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= 82 | github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= 83 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 84 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 85 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 86 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 87 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 88 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 89 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 90 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 91 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 92 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 93 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 94 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 95 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 96 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 97 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 98 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 99 | github.com/mccutchen/go-httpbin/v2 v2.18.3 h1:DyckIScjHLJtmlSju+rgjqqI1nL8AdMZHsLSljlbnMU= 100 | github.com/mccutchen/go-httpbin/v2 v2.18.3/go.mod h1:GBy5I7XwZ4ZLhT3hcq39I4ikwN9x4QUt6EAxNiR8Jus= 101 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd h1:AC3N94irbx2kWGA8f/2Ks7EQl2LxKIRQYuT9IJDwgiI= 102 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd/go.mod h1:9vRHVuLCjoFfE3GT06X0spdOAO+Zzo4AMjdIwUHBvAk= 103 | github.com/mstoykov/envconfig v1.5.0 h1:E2FgWf73BQt0ddgn7aoITkQHmgwAcHup1s//MsS5/f8= 104 | github.com/mstoykov/envconfig v1.5.0/go.mod h1:vk/d9jpexY2Z9Bb0uB4Ndesss1Sr0Z9ZiGUrg5o9VGk= 105 | github.com/mstoykov/k6-taskqueue-lib v0.1.3 h1:sdiSc5NEK/qpQkTQe505vgRYQocZevdO9ON+yMudFqo= 106 | github.com/mstoykov/k6-taskqueue-lib v0.1.3/go.mod h1:e9R2vtLFHCKT+CMiEjTJVMQiJAi17M1KiXXRs7FYc6w= 107 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= 108 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 109 | github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= 110 | github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= 111 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 112 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 113 | github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE= 114 | github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= 115 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 116 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 117 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 118 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 119 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= 120 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 121 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 122 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 123 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 124 | github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= 125 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 126 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 127 | github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= 128 | github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= 129 | github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= 130 | github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= 131 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 132 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 133 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE= 134 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= 135 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 136 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 137 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 138 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 139 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 140 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 141 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 142 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 143 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 144 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 145 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 146 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 147 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 148 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 149 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 150 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 151 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 152 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 153 | go.k6.io/k6 v1.4.2 h1:diX88uGPeIej39pW2/20DVEqjLB8qreCjG0h4A/5qwE= 154 | go.k6.io/k6 v1.4.2/go.mod h1:2tiFK3BmthsdOzb70NIp/U84cee7j71Z2DlQ/rELnkE= 155 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 156 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 157 | go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= 158 | go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= 159 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= 160 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= 161 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= 162 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= 163 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= 164 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= 165 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= 166 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= 167 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= 168 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= 169 | go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= 170 | go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= 171 | go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= 172 | go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= 173 | go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= 174 | go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= 175 | go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= 176 | go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 177 | go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= 178 | go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= 179 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 180 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 181 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 182 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 183 | golang.org/x/crypto/x509roots/fallback v0.0.0-20251009181029-0b7aa0cfb07b h1:YjNArlzCQB2fDkuKSxMwY1ZUQeRXFIFa23Ov9Wa7TUE= 184 | golang.org/x/crypto/x509roots/fallback v0.0.0-20251009181029-0b7aa0cfb07b/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18= 185 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 186 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 187 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 188 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 189 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 190 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 191 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 192 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 193 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 194 | golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 195 | golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 196 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 197 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 198 | golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 199 | golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 200 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 201 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 202 | google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= 203 | google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= 204 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= 205 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= 206 | google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= 207 | google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= 208 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 209 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 210 | gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= 211 | gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= 212 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 213 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 214 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 215 | gopkg.in/guregu/null.v3 v3.3.0 h1:8j3ggqq+NgKt/O7mbFVUFKUMWN+l1AmT5jQmJ6nPh2c= 216 | gopkg.in/guregu/null.v3 v3.3.0/go.mod h1:E4tX2Qe3h7QdL+uZ3a0vqvYwKQsRSQKM5V4YltdgH9Y= 217 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 218 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 219 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 220 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 221 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 222 | -------------------------------------------------------------------------------- /redis/client.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net" 8 | "time" 9 | 10 | "github.com/grafana/sobek" 11 | "github.com/redis/go-redis/v9" 12 | "go.k6.io/k6/js/common" 13 | "go.k6.io/k6/js/modules" 14 | "go.k6.io/k6/js/promises" 15 | "go.k6.io/k6/lib" 16 | ) 17 | 18 | // Client represents the Client constructor (i.e. `new redis.Client()`) and 19 | // returns a new Redis client object. 20 | type Client struct { 21 | vu modules.VU 22 | redisOptions *redis.UniversalOptions 23 | redisClient redis.UniversalClient 24 | } 25 | 26 | // Set the given key with the given value. 27 | // 28 | // If the provided value is not a supported type, the promise is rejected with an error. 29 | // 30 | // The value for `expiration` is interpreted as seconds. 31 | func (c *Client) Set(key string, value interface{}, expiration int) *sobek.Promise { 32 | promise, resolve, reject := promises.New(c.vu) 33 | 34 | if err := c.connect(); err != nil { 35 | reject(err) 36 | return promise 37 | } 38 | 39 | if err := c.isSupportedType(1, value); err != nil { 40 | reject(err) 41 | return promise 42 | } 43 | 44 | go func() { 45 | result, err := c.redisClient.Set(c.vu.Context(), key, value, time.Duration(expiration)*time.Second).Result() 46 | if err != nil { 47 | reject(err) 48 | return 49 | } 50 | 51 | resolve(result) 52 | }() 53 | 54 | return promise 55 | } 56 | 57 | // Get returns the value for the given key. 58 | // 59 | // If the key does not exist, the promise is rejected with an error. 60 | // 61 | // If the key does not exist, the promise is rejected with an error. 62 | func (c *Client) Get(key string) *sobek.Promise { 63 | promise, resolve, reject := promises.New(c.vu) 64 | 65 | if err := c.connect(); err != nil { 66 | reject(err) 67 | return promise 68 | } 69 | 70 | go func() { 71 | value, err := c.redisClient.Get(c.vu.Context(), key).Result() 72 | if err != nil { 73 | reject(err) 74 | return 75 | } 76 | 77 | resolve(value) 78 | }() 79 | 80 | return promise 81 | } 82 | 83 | // GetSet sets the value of key to value and returns the old value stored 84 | // 85 | // If the provided value is not a supported type, the promise is rejected with an error. 86 | func (c *Client) GetSet(key string, value interface{}) *sobek.Promise { 87 | promise, resolve, reject := promises.New(c.vu) 88 | 89 | if err := c.connect(); err != nil { 90 | reject(err) 91 | return promise 92 | } 93 | 94 | if err := c.isSupportedType(1, value); err != nil { 95 | reject(err) 96 | return promise 97 | } 98 | 99 | go func() { 100 | oldValue, err := c.redisClient.GetSet(c.vu.Context(), key, value).Result() 101 | if err != nil { 102 | reject(err) 103 | return 104 | } 105 | 106 | resolve(oldValue) 107 | }() 108 | 109 | return promise 110 | } 111 | 112 | // Del removes the specified keys. A key is ignored if it does not exist 113 | func (c *Client) Del(keys ...string) *sobek.Promise { 114 | promise, resolve, reject := promises.New(c.vu) 115 | 116 | if err := c.connect(); err != nil { 117 | reject(err) 118 | return promise 119 | } 120 | 121 | go func() { 122 | n, err := c.redisClient.Del(c.vu.Context(), keys...).Result() 123 | if err != nil { 124 | reject(err) 125 | return 126 | } 127 | 128 | resolve(n) 129 | }() 130 | 131 | return promise 132 | } 133 | 134 | // GetDel gets the value of key and deletes the key. 135 | // 136 | // If the key does not exist, the promise is rejected with an error. 137 | func (c *Client) GetDel(key string) *sobek.Promise { 138 | promise, resolve, reject := promises.New(c.vu) 139 | 140 | if err := c.connect(); err != nil { 141 | reject(err) 142 | return promise 143 | } 144 | 145 | go func() { 146 | value, err := c.redisClient.GetDel(c.vu.Context(), key).Result() 147 | if err != nil { 148 | reject(err) 149 | return 150 | } 151 | 152 | resolve(value) 153 | }() 154 | 155 | return promise 156 | } 157 | 158 | // Exists returns the number of key arguments that exist. 159 | // Note that if the same existing key is mentioned in the argument 160 | // multiple times, it will be counted multiple times. 161 | func (c *Client) Exists(keys ...string) *sobek.Promise { 162 | promise, resolve, reject := promises.New(c.vu) 163 | 164 | if err := c.connect(); err != nil { 165 | reject(err) 166 | return promise 167 | } 168 | 169 | go func() { 170 | n, err := c.redisClient.Exists(c.vu.Context(), keys...).Result() 171 | if err != nil { 172 | reject(err) 173 | return 174 | } 175 | 176 | resolve(n) 177 | }() 178 | 179 | return promise 180 | } 181 | 182 | // Incr increments the number stored at `key` by one. If the key does 183 | // not exist, it is set to zero before performing the operation. An 184 | // error is returned if the key contains a value of the wrong type, or 185 | // contains a string that cannot be represented as an integer. 186 | func (c *Client) Incr(key string) *sobek.Promise { 187 | promise, resolve, reject := promises.New(c.vu) 188 | 189 | if err := c.connect(); err != nil { 190 | reject(err) 191 | return promise 192 | } 193 | 194 | go func() { 195 | newValue, err := c.redisClient.Incr(c.vu.Context(), key).Result() 196 | if err != nil { 197 | reject(err) 198 | return 199 | } 200 | 201 | resolve(newValue) 202 | }() 203 | 204 | return promise 205 | } 206 | 207 | // IncrBy increments the number stored at `key` by `increment`. If the key does 208 | // not exist, it is set to zero before performing the operation. An 209 | // error is returned if the key contains a value of the wrong type, or 210 | // contains a string that cannot be represented as an integer. 211 | func (c *Client) IncrBy(key string, increment int64) *sobek.Promise { 212 | promise, resolve, reject := promises.New(c.vu) 213 | 214 | if err := c.connect(); err != nil { 215 | reject(err) 216 | return promise 217 | } 218 | 219 | go func() { 220 | newValue, err := c.redisClient.IncrBy(c.vu.Context(), key, increment).Result() 221 | if err != nil { 222 | reject(err) 223 | return 224 | } 225 | 226 | resolve(newValue) 227 | }() 228 | 229 | return promise 230 | } 231 | 232 | // Decr decrements the number stored at `key` by one. If the key does 233 | // not exist, it is set to zero before performing the operation. An 234 | // error is returned if the key contains a value of the wrong type, or 235 | // contains a string that cannot be represented as an integer. 236 | func (c *Client) Decr(key string) *sobek.Promise { 237 | promise, resolve, reject := promises.New(c.vu) 238 | 239 | if err := c.connect(); err != nil { 240 | reject(err) 241 | return promise 242 | } 243 | 244 | go func() { 245 | newValue, err := c.redisClient.Decr(c.vu.Context(), key).Result() 246 | if err != nil { 247 | reject(err) 248 | return 249 | } 250 | 251 | resolve(newValue) 252 | }() 253 | 254 | return promise 255 | } 256 | 257 | // DecrBy decrements the number stored at `key` by `decrement`. If the key does 258 | // not exist, it is set to zero before performing the operation. An 259 | // error is returned if the key contains a value of the wrong type, or 260 | // contains a string that cannot be represented as an integer. 261 | func (c *Client) DecrBy(key string, decrement int64) *sobek.Promise { 262 | promise, resolve, reject := promises.New(c.vu) 263 | 264 | if err := c.connect(); err != nil { 265 | reject(err) 266 | return promise 267 | } 268 | 269 | go func() { 270 | newValue, err := c.redisClient.DecrBy(c.vu.Context(), key, decrement).Result() 271 | if err != nil { 272 | reject(err) 273 | return 274 | } 275 | 276 | resolve(newValue) 277 | }() 278 | 279 | return promise 280 | } 281 | 282 | // RandomKey returns a random key. 283 | // 284 | // If the database is empty, the promise is rejected with an error. 285 | func (c *Client) RandomKey() *sobek.Promise { 286 | promise, resolve, reject := promises.New(c.vu) 287 | 288 | if err := c.connect(); err != nil { 289 | reject(err) 290 | return promise 291 | } 292 | 293 | go func() { 294 | key, err := c.redisClient.RandomKey(c.vu.Context()).Result() 295 | if err != nil { 296 | reject(err) 297 | return 298 | } 299 | 300 | resolve(key) 301 | }() 302 | 303 | return promise 304 | } 305 | 306 | // Mget returns the values associated with the specified keys. 307 | func (c *Client) Mget(keys ...string) *sobek.Promise { 308 | promise, resolve, reject := promises.New(c.vu) 309 | 310 | if err := c.connect(); err != nil { 311 | reject(err) 312 | return promise 313 | } 314 | 315 | go func() { 316 | values, err := c.redisClient.MGet(c.vu.Context(), keys...).Result() 317 | if err != nil { 318 | reject(err) 319 | return 320 | } 321 | 322 | resolve(values) 323 | }() 324 | 325 | return promise 326 | } 327 | 328 | // Expire sets a timeout on key, after which the key will automatically 329 | // be deleted. 330 | // Note that calling Expire with a non-positive timeout will result in 331 | // the key being deleted rather than expired. 332 | func (c *Client) Expire(key string, seconds int) *sobek.Promise { 333 | promise, resolve, reject := promises.New(c.vu) 334 | 335 | if err := c.connect(); err != nil { 336 | reject(err) 337 | return promise 338 | } 339 | 340 | go func() { 341 | ok, err := c.redisClient.Expire(c.vu.Context(), key, time.Duration(seconds)*time.Second).Result() 342 | if err != nil { 343 | reject(err) 344 | return 345 | } 346 | 347 | resolve(ok) 348 | }() 349 | 350 | return promise 351 | } 352 | 353 | // Ttl returns the remaining time to live of a key that has a timeout. 354 | // 355 | //nolint:revive 356 | func (c *Client) Ttl(key string) *sobek.Promise { 357 | promise, resolve, reject := promises.New(c.vu) 358 | 359 | if err := c.connect(); err != nil { 360 | reject(err) 361 | return promise 362 | } 363 | 364 | go func() { 365 | duration, err := c.redisClient.TTL(c.vu.Context(), key).Result() 366 | if err != nil { 367 | reject(err) 368 | return 369 | } 370 | 371 | resolve(duration.Seconds()) 372 | }() 373 | 374 | return promise 375 | } 376 | 377 | // Persist removes the existing timeout on key. 378 | func (c *Client) Persist(key string) *sobek.Promise { 379 | promise, resolve, reject := promises.New(c.vu) 380 | 381 | if err := c.connect(); err != nil { 382 | reject(err) 383 | return promise 384 | } 385 | 386 | go func() { 387 | ok, err := c.redisClient.Persist(c.vu.Context(), key).Result() 388 | if err != nil { 389 | reject(err) 390 | return 391 | } 392 | 393 | resolve(ok) 394 | }() 395 | 396 | return promise 397 | } 398 | 399 | // Lpush inserts all the specified values at the head of the list stored 400 | // at `key`. If `key` does not exist, it is created as empty list before 401 | // performing the push operations. When `key` holds a value that is not 402 | // a list, and error is returned. 403 | func (c *Client) Lpush(key string, values ...interface{}) *sobek.Promise { 404 | promise, resolve, reject := promises.New(c.vu) 405 | 406 | if err := c.connect(); err != nil { 407 | reject(err) 408 | return promise 409 | } 410 | 411 | if err := c.isSupportedType(1, values...); err != nil { 412 | reject(err) 413 | return promise 414 | } 415 | 416 | go func() { 417 | listLength, err := c.redisClient.LPush(c.vu.Context(), key, values...).Result() 418 | if err != nil { 419 | reject(err) 420 | return 421 | } 422 | 423 | resolve(listLength) 424 | }() 425 | 426 | return promise 427 | } 428 | 429 | // Rpush inserts all the specified values at the tail of the list stored 430 | // at `key`. If `key` does not exist, it is created as empty list before 431 | // performing the push operations. 432 | func (c *Client) Rpush(key string, values ...interface{}) *sobek.Promise { 433 | promise, resolve, reject := promises.New(c.vu) 434 | 435 | if err := c.connect(); err != nil { 436 | reject(err) 437 | return promise 438 | } 439 | 440 | if err := c.isSupportedType(1, values...); err != nil { 441 | reject(err) 442 | return promise 443 | } 444 | 445 | go func() { 446 | listLength, err := c.redisClient.RPush(c.vu.Context(), key, values...).Result() 447 | if err != nil { 448 | reject(err) 449 | return 450 | } 451 | 452 | resolve(listLength) 453 | }() 454 | 455 | return promise 456 | } 457 | 458 | // Lpop removes and returns the first element of the list stored at `key`. 459 | // 460 | // If the list does not exist, this command rejects the promise with an error. 461 | func (c *Client) Lpop(key string) *sobek.Promise { 462 | // TODO: redis supports indicating the amount of values to pop 463 | promise, resolve, reject := promises.New(c.vu) 464 | 465 | if err := c.connect(); err != nil { 466 | reject(err) 467 | return promise 468 | } 469 | 470 | go func() { 471 | value, err := c.redisClient.LPop(c.vu.Context(), key).Result() 472 | if err != nil { 473 | reject(err) 474 | return 475 | } 476 | 477 | resolve(value) 478 | }() 479 | 480 | return promise 481 | } 482 | 483 | // Rpop removes and returns the last element of the list stored at `key`. 484 | // 485 | // If the list does not exist, this command rejects the promise with an error. 486 | func (c *Client) Rpop(key string) *sobek.Promise { 487 | // TODO: redis supports indicating the amount of values to pop 488 | promise, resolve, reject := promises.New(c.vu) 489 | 490 | if err := c.connect(); err != nil { 491 | reject(err) 492 | return promise 493 | } 494 | 495 | go func() { 496 | value, err := c.redisClient.RPop(c.vu.Context(), key).Result() 497 | if err != nil { 498 | reject(err) 499 | return 500 | } 501 | 502 | resolve(value) 503 | }() 504 | 505 | return promise 506 | } 507 | 508 | // Lrange returns the specified elements of the list stored at `key`. The 509 | // offsets start and stop are zero-based indexes. These offsets can be 510 | // negative numbers, where they indicate offsets starting at the end of 511 | // the list. 512 | func (c *Client) Lrange(key string, start, stop int64) *sobek.Promise { 513 | promise, resolve, reject := promises.New(c.vu) 514 | 515 | if err := c.connect(); err != nil { 516 | reject(err) 517 | return promise 518 | } 519 | 520 | go func() { 521 | values, err := c.redisClient.LRange(c.vu.Context(), key, start, stop).Result() 522 | if err != nil { 523 | reject(err) 524 | return 525 | } 526 | 527 | resolve(values) 528 | }() 529 | 530 | return promise 531 | } 532 | 533 | // Lindex returns the specified element of the list stored at `key`. 534 | // The index is zero-based. Negative indices can be used to designate 535 | // elements starting at the tail of the list. 536 | // 537 | // If the list does not exist, this command rejects the promise with an error. 538 | func (c *Client) Lindex(key string, index int64) *sobek.Promise { 539 | promise, resolve, reject := promises.New(c.vu) 540 | 541 | if err := c.connect(); err != nil { 542 | reject(err) 543 | return promise 544 | } 545 | 546 | go func() { 547 | value, err := c.redisClient.LIndex(c.vu.Context(), key, index).Result() 548 | if err != nil { 549 | reject(err) 550 | return 551 | } 552 | 553 | resolve(value) 554 | }() 555 | 556 | return promise 557 | } 558 | 559 | // Lset sets the list element at `index` to `element`. 560 | // 561 | // If the list does not exist, this command rejects the promise with an error. 562 | func (c *Client) Lset(key string, index int64, element string) *sobek.Promise { 563 | promise, resolve, reject := promises.New(c.vu) 564 | 565 | if err := c.connect(); err != nil { 566 | reject(err) 567 | return promise 568 | } 569 | 570 | go func() { 571 | value, err := c.redisClient.LSet(c.vu.Context(), key, index, element).Result() 572 | if err != nil { 573 | reject(err) 574 | return 575 | } 576 | 577 | resolve(value) 578 | }() 579 | 580 | return promise 581 | } 582 | 583 | // Lrem removes the first `count` occurrences of `value` from the list stored 584 | // at `key`. If `count` is positive, elements are removed from the beginning of the list. 585 | // If `count` is negative, elements are removed from the end of the list. 586 | // If `count` is zero, all elements matching `value` are removed. 587 | // 588 | // If the list does not exist, this command rejects the promise with an error. 589 | func (c *Client) Lrem(key string, count int64, value string) *sobek.Promise { 590 | promise, resolve, reject := promises.New(c.vu) 591 | 592 | if err := c.connect(); err != nil { 593 | reject(err) 594 | return promise 595 | } 596 | 597 | go func() { 598 | n, err := c.redisClient.LRem(c.vu.Context(), key, count, value).Result() 599 | if err != nil { 600 | reject(err) 601 | return 602 | } 603 | 604 | resolve(n) 605 | }() 606 | 607 | return promise 608 | } 609 | 610 | // Llen returns the length of the list stored at `key`. If `key` 611 | // does not exist, it is interpreted as an empty list and 0 is returned. 612 | // 613 | // If the list does not exist, this command rejects the promise with an error. 614 | func (c *Client) Llen(key string) *sobek.Promise { 615 | promise, resolve, reject := promises.New(c.vu) 616 | 617 | if err := c.connect(); err != nil { 618 | reject(err) 619 | return promise 620 | } 621 | 622 | go func() { 623 | length, err := c.redisClient.LLen(c.vu.Context(), key).Result() 624 | if err != nil { 625 | reject(err) 626 | return 627 | } 628 | 629 | resolve(length) 630 | }() 631 | 632 | return promise 633 | } 634 | 635 | // Hset sets the specified field in the hash stored at `key` to `value`. 636 | // If the `key` does not exist, a new key holding a hash is created. 637 | // If `field` already exists in the hash, it is overwritten. 638 | // 639 | // If the hash does not exist, this command rejects the promise with an error. 640 | func (c *Client) Hset(key string, field string, value interface{}) *sobek.Promise { 641 | promise, resolve, reject := promises.New(c.vu) 642 | 643 | if err := c.connect(); err != nil { 644 | reject(err) 645 | return promise 646 | } 647 | 648 | if err := c.isSupportedType(2, value); err != nil { 649 | reject(err) 650 | return promise 651 | } 652 | 653 | go func() { 654 | n, err := c.redisClient.HSet(c.vu.Context(), key, field, value).Result() 655 | if err != nil { 656 | reject(err) 657 | return 658 | } 659 | 660 | resolve(n) 661 | }() 662 | 663 | return promise 664 | } 665 | 666 | // Hsetnx sets the specified field in the hash stored at `key` to `value`, 667 | // only if `field` does not yet exist. If `key` does not exist, a new key 668 | // holding a hash is created. If `field` already exists, this operation 669 | // has no effect. 670 | func (c *Client) Hsetnx(key, field, value string) *sobek.Promise { 671 | promise, resolve, reject := promises.New(c.vu) 672 | 673 | if err := c.connect(); err != nil { 674 | reject(err) 675 | return promise 676 | } 677 | 678 | go func() { 679 | ok, err := c.redisClient.HSetNX(c.vu.Context(), key, field, value).Result() 680 | if err != nil { 681 | reject(err) 682 | return 683 | } 684 | 685 | resolve(ok) 686 | }() 687 | 688 | return promise 689 | } 690 | 691 | // Hget returns the value associated with `field` in the hash stored at `key`. 692 | // 693 | // If the hash does not exist, this command rejects the promise with an error. 694 | func (c *Client) Hget(key, field string) *sobek.Promise { 695 | promise, resolve, reject := promises.New(c.vu) 696 | 697 | if err := c.connect(); err != nil { 698 | reject(err) 699 | return promise 700 | } 701 | 702 | go func() { 703 | value, err := c.redisClient.HGet(c.vu.Context(), key, field).Result() 704 | if err != nil { 705 | reject(err) 706 | return 707 | } 708 | 709 | resolve(value) 710 | }() 711 | 712 | return promise 713 | } 714 | 715 | // Hdel deletes the specified fields from the hash stored at `key`. 716 | func (c *Client) Hdel(key string, fields ...string) *sobek.Promise { 717 | promise, resolve, reject := promises.New(c.vu) 718 | 719 | if err := c.connect(); err != nil { 720 | reject(err) 721 | return promise 722 | } 723 | 724 | go func() { 725 | n, err := c.redisClient.HDel(c.vu.Context(), key, fields...).Result() 726 | if err != nil { 727 | reject(err) 728 | return 729 | } 730 | 731 | resolve(n) 732 | }() 733 | 734 | return promise 735 | } 736 | 737 | // Hgetall returns all fields and values of the hash stored at `key`. 738 | // 739 | // If the hash does not exist, this command rejects the promise with an error. 740 | func (c *Client) Hgetall(key string) *sobek.Promise { 741 | promise, resolve, reject := promises.New(c.vu) 742 | 743 | if err := c.connect(); err != nil { 744 | reject(err) 745 | return promise 746 | } 747 | 748 | go func() { 749 | hashMap, err := c.redisClient.HGetAll(c.vu.Context(), key).Result() 750 | if err != nil { 751 | reject(err) 752 | return 753 | } 754 | 755 | resolve(hashMap) 756 | }() 757 | 758 | return promise 759 | } 760 | 761 | // Hkeys returns all fields of the hash stored at `key`. 762 | // 763 | // If the hash does not exist, this command rejects the promise with an error. 764 | func (c *Client) Hkeys(key string) *sobek.Promise { 765 | promise, resolve, reject := promises.New(c.vu) 766 | 767 | if err := c.connect(); err != nil { 768 | reject(err) 769 | return promise 770 | } 771 | 772 | go func() { 773 | keys, err := c.redisClient.HKeys(c.vu.Context(), key).Result() 774 | if err != nil { 775 | reject(err) 776 | return 777 | } 778 | 779 | resolve(keys) 780 | }() 781 | 782 | return promise 783 | } 784 | 785 | // Hvals returns all values of the hash stored at `key`. 786 | // 787 | // If the hash does not exist, this command rejects the promise with an error. 788 | func (c *Client) Hvals(key string) *sobek.Promise { 789 | promise, resolve, reject := promises.New(c.vu) 790 | 791 | if err := c.connect(); err != nil { 792 | reject(err) 793 | return promise 794 | } 795 | 796 | go func() { 797 | values, err := c.redisClient.HVals(c.vu.Context(), key).Result() 798 | if err != nil { 799 | reject(err) 800 | return 801 | } 802 | 803 | resolve(values) 804 | }() 805 | 806 | return promise 807 | } 808 | 809 | // Hlen returns the number of fields in the hash stored at `key`. 810 | // 811 | // If the hash does not exist, this command rejects the promise with an error. 812 | func (c *Client) Hlen(key string) *sobek.Promise { 813 | promise, resolve, reject := promises.New(c.vu) 814 | 815 | if err := c.connect(); err != nil { 816 | reject(err) 817 | return promise 818 | } 819 | 820 | go func() { 821 | n, err := c.redisClient.HLen(c.vu.Context(), key).Result() 822 | if err != nil { 823 | reject(err) 824 | return 825 | } 826 | 827 | resolve(n) 828 | }() 829 | 830 | return promise 831 | } 832 | 833 | // Hincrby increments the integer value of `field` in the hash stored at `key` 834 | // by `increment`. If `key` does not exist, a new key holding a hash is created. 835 | // If `field` does not exist the value is set to 0 before the operation is 836 | // set to 0 before the operation is performed. 837 | func (c *Client) Hincrby(key, field string, increment int64) *sobek.Promise { 838 | promise, resolve, reject := promises.New(c.vu) 839 | 840 | if err := c.connect(); err != nil { 841 | reject(err) 842 | return promise 843 | } 844 | 845 | go func() { 846 | newValue, err := c.redisClient.HIncrBy(c.vu.Context(), key, field, increment).Result() 847 | if err != nil { 848 | reject(err) 849 | return 850 | } 851 | 852 | resolve(newValue) 853 | }() 854 | 855 | return promise 856 | } 857 | 858 | // Sadd adds the specified members to the set stored at key. 859 | // Specified members that are already a member of this set are ignored. 860 | // If key does not exist, a new set is created before adding the specified members. 861 | func (c *Client) Sadd(key string, members ...interface{}) *sobek.Promise { 862 | promise, resolve, reject := promises.New(c.vu) 863 | 864 | if err := c.connect(); err != nil { 865 | reject(err) 866 | return promise 867 | } 868 | 869 | if err := c.isSupportedType(1, members...); err != nil { 870 | reject(err) 871 | return promise 872 | } 873 | 874 | go func() { 875 | n, err := c.redisClient.SAdd(c.vu.Context(), key, members...).Result() 876 | if err != nil { 877 | reject(err) 878 | return 879 | } 880 | 881 | resolve(n) 882 | }() 883 | 884 | return promise 885 | } 886 | 887 | // Srem removes the specified members from the set stored at key. 888 | // Specified members that are not a member of this set are ignored. 889 | // If key does not exist, it is treated as an empty set and this command returns 0. 890 | func (c *Client) Srem(key string, members ...interface{}) *sobek.Promise { 891 | promise, resolve, reject := promises.New(c.vu) 892 | 893 | if err := c.connect(); err != nil { 894 | reject(err) 895 | return promise 896 | } 897 | 898 | if err := c.isSupportedType(1, members...); err != nil { 899 | reject(err) 900 | return promise 901 | } 902 | 903 | go func() { 904 | n, err := c.redisClient.SRem(c.vu.Context(), key, members...).Result() 905 | if err != nil { 906 | reject(err) 907 | return 908 | } 909 | 910 | resolve(n) 911 | }() 912 | 913 | return promise 914 | } 915 | 916 | // Sismember returns if member is a member of the set stored at key. 917 | func (c *Client) Sismember(key string, member interface{}) *sobek.Promise { 918 | promise, resolve, reject := promises.New(c.vu) 919 | 920 | if err := c.connect(); err != nil { 921 | reject(err) 922 | return promise 923 | } 924 | 925 | if err := c.isSupportedType(1, member); err != nil { 926 | reject(err) 927 | return promise 928 | } 929 | 930 | go func() { 931 | ok, err := c.redisClient.SIsMember(c.vu.Context(), key, member).Result() 932 | if err != nil { 933 | reject(err) 934 | return 935 | } 936 | 937 | resolve(ok) 938 | }() 939 | 940 | return promise 941 | } 942 | 943 | // Smembers returns all members of the set stored at key. 944 | func (c *Client) Smembers(key string) *sobek.Promise { 945 | promise, resolve, reject := promises.New(c.vu) 946 | 947 | if err := c.connect(); err != nil { 948 | reject(err) 949 | return promise 950 | } 951 | 952 | go func() { 953 | members, err := c.redisClient.SMembers(c.vu.Context(), key).Result() 954 | if err != nil { 955 | reject(err) 956 | return 957 | } 958 | 959 | resolve(members) 960 | }() 961 | 962 | return promise 963 | } 964 | 965 | // Srandmember returns a random element from the set value stored at key. 966 | // 967 | // If the set does not exist, the promise is rejected with an error. 968 | func (c *Client) Srandmember(key string) *sobek.Promise { 969 | promise, resolve, reject := promises.New(c.vu) 970 | 971 | if err := c.connect(); err != nil { 972 | reject(err) 973 | return promise 974 | } 975 | 976 | go func() { 977 | element, err := c.redisClient.SRandMember(c.vu.Context(), key).Result() 978 | if err != nil { 979 | reject(err) 980 | return 981 | } 982 | 983 | resolve(element) 984 | }() 985 | 986 | return promise 987 | } 988 | 989 | // Spop removes and returns a random element from the set value stored at key. 990 | // 991 | // If the set does not exist, the promise is rejected with an error. 992 | func (c *Client) Spop(key string) *sobek.Promise { 993 | promise, resolve, reject := promises.New(c.vu) 994 | 995 | if err := c.connect(); err != nil { 996 | reject(err) 997 | return promise 998 | } 999 | 1000 | go func() { 1001 | element, err := c.redisClient.SPop(c.vu.Context(), key).Result() 1002 | if err != nil { 1003 | reject(err) 1004 | return 1005 | } 1006 | 1007 | resolve(element) 1008 | }() 1009 | 1010 | return promise 1011 | } 1012 | 1013 | // SendCommand sends a command to the redis server. 1014 | func (c *Client) SendCommand(command string, args ...interface{}) *sobek.Promise { 1015 | var doArgs []interface{} 1016 | doArgs = append(doArgs, command) 1017 | doArgs = append(doArgs, args...) 1018 | 1019 | promise, resolve, reject := promises.New(c.vu) 1020 | 1021 | if err := c.connect(); err != nil { 1022 | reject(err) 1023 | return promise 1024 | } 1025 | 1026 | if err := c.isSupportedType(1, args...); err != nil { 1027 | reject(err) 1028 | return promise 1029 | } 1030 | 1031 | go func() { 1032 | cmd, err := c.redisClient.Do(c.vu.Context(), doArgs...).Result() 1033 | if err != nil { 1034 | reject(err) 1035 | return 1036 | } 1037 | 1038 | resolve(cmd) 1039 | }() 1040 | 1041 | return promise 1042 | } 1043 | 1044 | // connect establishes the client's connection to the target 1045 | // redis instance(s). 1046 | func (c *Client) connect() error { 1047 | // A nil VU state indicates we are in the init context. 1048 | // As a general convention, k6 should not perform IO in the 1049 | // init context. Thus, the Connect method will error if 1050 | // called in the init context. 1051 | vuState := c.vu.State() 1052 | if vuState == nil { 1053 | return common.NewInitContextError("connecting to a redis server in the init context is not supported") 1054 | } 1055 | 1056 | // If the redisClient is already instantiated, it is safe 1057 | // to assume that the connection is already established. 1058 | if c.redisClient != nil { 1059 | return nil 1060 | } 1061 | 1062 | tlsCfg := c.redisOptions.TLSConfig 1063 | if tlsCfg != nil && vuState.TLSConfig != nil { 1064 | // Merge k6 TLS configuration with the one we received from the 1065 | // Client constructor. This will need adjusting depending on which 1066 | // options we want to expose in the Redis module, and how we want 1067 | // the override to work. 1068 | tlsCfg.InsecureSkipVerify = vuState.TLSConfig.InsecureSkipVerify 1069 | tlsCfg.CipherSuites = vuState.TLSConfig.CipherSuites 1070 | tlsCfg.MinVersion = vuState.TLSConfig.MinVersion 1071 | tlsCfg.MaxVersion = vuState.TLSConfig.MaxVersion 1072 | tlsCfg.Renegotiation = vuState.TLSConfig.Renegotiation 1073 | tlsCfg.KeyLogWriter = vuState.TLSConfig.KeyLogWriter 1074 | tlsCfg.Certificates = append(tlsCfg.Certificates, vuState.TLSConfig.Certificates...) 1075 | 1076 | // TODO: Merge vuState.TLSConfig.RootCAs with 1077 | // c.redisOptions.TLSConfig. k6 currently doesn't allow setting 1078 | // this, so it doesn't matter right now, but these should be merged. 1079 | // I couldn't find a way to do this with the x509.CertPool API 1080 | // though... 1081 | 1082 | // In order to preserve the underlying effects of the [netext.Dialer], such 1083 | // as handling blocked hostnames, or handling hostname resolution, we override 1084 | // the redis client's dialer with our own function which uses the VU's [netext.Dialer] 1085 | // and manually upgrades the connection to TLS. 1086 | // 1087 | // See Pull Request's #17 [discussion] for more details. 1088 | // 1089 | // [discussion]: https://github.com/grafana/xk6-redis/pull/17#discussion_r1369707388 1090 | c.redisOptions.Dialer = c.upgradeDialerToTLS(vuState.Dialer, tlsCfg) 1091 | } else { 1092 | c.redisOptions.Dialer = vuState.Dialer.DialContext 1093 | } 1094 | 1095 | // Replace the internal redis client instance with a new 1096 | // one using our custom options. 1097 | c.redisClient = redis.NewUniversalClient(c.redisOptions) 1098 | 1099 | return nil 1100 | } 1101 | 1102 | // IsConnected returns true if the client is connected to redis. 1103 | func (c *Client) IsConnected() bool { 1104 | return c.redisClient != nil 1105 | } 1106 | 1107 | // isSupportedType returns whether the provided arguments are of a type 1108 | // supported by the redis client. 1109 | // 1110 | // Errors will indicate the zero-indexed position of the argument of 1111 | // an unsuppoprted type. 1112 | // 1113 | // isSupportedType should report type errors with arguments in the correct 1114 | // position. To be able to accurately report the argument position in the larger 1115 | // context of a call to a redis function, the `offset` argument allows to indicate 1116 | // the amount of arguments present in front of the ones we provide to `isSupportedType`. 1117 | // For instance, when calling `set`, which takes a key, and a value argument, 1118 | // isSupportedType applied to the value should eventually report an error with 1119 | // the argument in position 1. 1120 | func (c *Client) isSupportedType(offset int, args ...interface{}) error { 1121 | for idx, arg := range args { 1122 | switch arg.(type) { 1123 | case string, int, int64, float64, bool: 1124 | continue 1125 | default: 1126 | return fmt.Errorf( 1127 | "unsupported type provided for argument at index %d, "+ 1128 | "supported types are string, number, and boolean", idx+offset) 1129 | } 1130 | } 1131 | 1132 | return nil 1133 | } 1134 | 1135 | // DialContextFunc is a function that can be used to dial a connection to a redis server. 1136 | type DialContextFunc func(ctx context.Context, network, addr string) (net.Conn, error) 1137 | 1138 | // upgradeDialerToTLS returns a DialContextFunc that uses the provided dialer to 1139 | // establish a connection, and then upgrades it to TLS using the provided config. 1140 | // 1141 | // We use this function to make sure the k6 [netext.Dialer], our redis module uses to establish 1142 | // the connection and handle network-related options such as blocked hostnames, 1143 | // or hostname resolution, but we also want to use the TLS configuration provided 1144 | // by the user. 1145 | func (c *Client) upgradeDialerToTLS(dialer lib.DialContexter, config *tls.Config) DialContextFunc { 1146 | return func(ctx context.Context, network string, addr string) (net.Conn, error) { 1147 | // Use netext.Dialer to establish the connection 1148 | rawConn, err := dialer.DialContext(ctx, network, addr) 1149 | if err != nil { 1150 | return nil, err 1151 | } 1152 | 1153 | // Upgrade the connection to TLS if needed 1154 | tlsConn := tls.Client(rawConn, config) 1155 | err = tlsConn.HandshakeContext(ctx) 1156 | if err != nil { 1157 | if closeErr := rawConn.Close(); closeErr != nil { 1158 | return nil, fmt.Errorf("failed to close connection after TLS handshake error: %w", closeErr) 1159 | } 1160 | 1161 | return nil, err 1162 | } 1163 | 1164 | // Overwrite rawConn with the TLS connection 1165 | rawConn = tlsConn 1166 | 1167 | return rawConn, nil 1168 | } 1169 | } 1170 | -------------------------------------------------------------------------------- /redis/client_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/grafana/sobek" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | "go.k6.io/k6/js/modulestest" 15 | "go.k6.io/k6/lib" 16 | "go.k6.io/k6/lib/netext" 17 | "go.k6.io/k6/lib/types" 18 | "go.k6.io/k6/metrics" 19 | "gopkg.in/guregu/null.v3" 20 | ) 21 | 22 | func TestClientConstructor(t *testing.T) { 23 | t.Parallel() 24 | 25 | testCases := []struct { 26 | name, arg, expErr string 27 | }{ 28 | { 29 | name: "ok/url/tcp", 30 | arg: "'redis://user:pass@localhost:6379/0'", 31 | }, 32 | { 33 | name: "ok/url/tls", 34 | arg: "'rediss://somesecurehost'", 35 | }, 36 | { 37 | name: "ok/object/single", 38 | arg: `{ 39 | clientName: 'myclient', 40 | username: 'user', 41 | password: 'pass', 42 | socket: { 43 | host: 'localhost', 44 | port: 6379, 45 | } 46 | }`, 47 | }, 48 | { 49 | name: "ok/object/single_tls", 50 | arg: `{ 51 | socket: { 52 | host: 'localhost', 53 | port: 6379, 54 | tls: { 55 | ca: ['...'], 56 | } 57 | } 58 | }`, 59 | }, 60 | { 61 | name: "ok/object/cluster_urls", 62 | arg: `{ 63 | cluster: { 64 | maxRedirects: 3, 65 | readOnly: true, 66 | routeByLatency: true, 67 | routeRandomly: true, 68 | nodes: ['redis://host1:6379', 'redis://host2:6379'] 69 | } 70 | }`, 71 | }, 72 | { 73 | name: "ok/object/cluster_objects", 74 | arg: `{ 75 | cluster: { 76 | nodes: [ 77 | { 78 | username: 'user', 79 | password: 'pass', 80 | socket: { 81 | host: 'host1', 82 | port: 6379, 83 | }, 84 | }, 85 | { 86 | username: 'user', 87 | password: 'pass', 88 | socket: { 89 | host: 'host2', 90 | port: 6379, 91 | }, 92 | } 93 | ] 94 | } 95 | }`, 96 | }, 97 | { 98 | name: "ok/object/sentinel", 99 | arg: `{ 100 | username: 'user', 101 | password: 'pass', 102 | socket: { 103 | host: 'localhost', 104 | port: 6379, 105 | }, 106 | masterName: 'masterhost', 107 | sentinelUsername: 'sentineluser', 108 | sentinelPassword: 'sentinelpass', 109 | }`, 110 | }, 111 | { 112 | name: "err/empty", 113 | arg: "", 114 | expErr: "must specify one argument", 115 | }, 116 | { 117 | name: "err/url/missing_scheme", 118 | arg: "'localhost:6379'", 119 | expErr: "invalid URL scheme", 120 | }, 121 | { 122 | name: "err/url/invalid_scheme", 123 | arg: "'https://localhost:6379'", 124 | expErr: "invalid options; reason: redis: invalid URL scheme: https", 125 | }, 126 | { 127 | name: "err/object/unknown_field", 128 | arg: "{addrs: ['localhost:6379']}", 129 | expErr: `invalid options; reason: json: unknown field "addrs"`, 130 | }, 131 | { 132 | name: "err/object/empty_socket", 133 | arg: `{ 134 | username: 'user', 135 | password: 'pass', 136 | }`, 137 | expErr: "invalid options; reason: empty socket options", 138 | }, 139 | { 140 | name: "err/object/cluster_wrong_type", 141 | arg: `{ 142 | cluster: { 143 | nodes: 1, 144 | } 145 | }`, 146 | expErr: `invalid options; reason: cluster nodes property must be an array; got int64`, 147 | }, 148 | { 149 | name: "err/object/cluster_wrong_type_internal", 150 | arg: `{ 151 | cluster: { 152 | nodes: [1, 2], 153 | } 154 | }`, 155 | expErr: `invalid options; reason: cluster nodes array must contain string or object elements; got int64`, 156 | }, 157 | { 158 | name: "err/object/cluster_empty", 159 | arg: `{ 160 | cluster: { 161 | nodes: [] 162 | } 163 | }`, 164 | expErr: `invalid options; reason: cluster nodes property cannot be empty`, 165 | }, 166 | { 167 | name: "err/object/cluster_inconsistent_option", 168 | arg: `{ 169 | cluster: { 170 | nodes: [ 171 | { 172 | username: 'user1', 173 | password: 'pass', 174 | socket: { 175 | host: 'host1', 176 | port: 6379, 177 | }, 178 | }, 179 | { 180 | username: 'user2', 181 | password: 'pass', 182 | socket: { 183 | host: 'host2', 184 | port: 6379, 185 | }, 186 | } 187 | ] 188 | } 189 | }`, 190 | expErr: `invalid options; reason: inconsistent username option: user1 != user2`, 191 | }, 192 | } 193 | 194 | for _, tc := range testCases { 195 | t.Run(tc.name, func(t *testing.T) { 196 | t.Parallel() 197 | 198 | ts := newTestSetup(t) 199 | script := fmt.Sprintf("new Client(%s);", tc.arg) 200 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 201 | _, err := ts.rt.RunString(script) 202 | return err 203 | }) 204 | if tc.expErr != "" { 205 | require.Error(t, gotScriptErr) 206 | assert.Contains(t, gotScriptErr.Error(), tc.expErr) 207 | } else { 208 | assert.NoError(t, gotScriptErr) 209 | } 210 | }) 211 | } 212 | } 213 | 214 | func TestClientSet(t *testing.T) { 215 | t.Parallel() 216 | 217 | ts := newTestSetup(t) 218 | rs := RunT(t) 219 | rs.RegisterCommandHandler("SET", func(c *Connection, args []string) { 220 | if len(args) <= 2 && len(args) > 4 { 221 | c.WriteError(errors.New("ERR unexpected number of arguments for 'GET' command")) 222 | return 223 | } 224 | 225 | switch args[0] { 226 | case "existing_key", "non_existing_key": //nolint:goconst 227 | c.WriteOK() 228 | case "expires": 229 | if len(args) != 4 && args[2] != "EX" && args[3] != "0" { 230 | c.WriteError(errors.New("ERR unexpected number of arguments for 'SET' command")) 231 | } 232 | c.WriteOK() 233 | } 234 | }) 235 | 236 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 237 | _, err := ts.rt.RunString(fmt.Sprintf(` 238 | const redis = new Client('redis://%s'); 239 | 240 | redis.set("existing_key", "new_value") 241 | .then(res => { if (res !== "OK") { throw 'unexpected value for set result: ' + res } }) 242 | .then(() => redis.set("non_existing_key", "some_value")) 243 | .then(res => { if (res !== "OK") { throw 'unexpected value for set result: ' + res } }) 244 | .then(() => redis.set("expires", "expired", 10)) 245 | .then(res => { if (res !== "OK") { throw 'unexpected value for set result: ' + res } }) 246 | .then(() => redis.set("unsupported_type", new Array("unsupported"))) 247 | .then( 248 | res => { throw 'expected to fail setting unsupported type' }, 249 | err => { if (!err.error().startsWith('unsupported type')) { throw 'unexpected error: ' + err } } 250 | ) 251 | `, rs.Addr())) 252 | 253 | return err 254 | }) 255 | 256 | assert.NoError(t, gotScriptErr) 257 | assert.Equal(t, 3, rs.HandledCommandsCount()) 258 | assert.Equal(t, [][]string{ 259 | {"HELLO", "2"}, 260 | {"SET", "existing_key", "new_value"}, 261 | {"SET", "non_existing_key", "some_value"}, 262 | {"SET", "expires", "expired", "ex", "10"}, 263 | }, rs.GotCommands()) 264 | } 265 | 266 | func TestClientGet(t *testing.T) { 267 | t.Parallel() 268 | 269 | ts := newTestSetup(t) 270 | rs := RunT(t) 271 | rs.RegisterCommandHandler("GET", func(c *Connection, args []string) { 272 | if len(args) != 1 { 273 | c.WriteError(errors.New("ERR unexpected number of arguments for 'GET' command")) 274 | return 275 | } 276 | 277 | switch args[0] { 278 | case "existing_key": 279 | c.WriteBulkString("old_value") 280 | case "non_existing_key": 281 | c.WriteNull() 282 | } 283 | }) 284 | 285 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 286 | _, err := ts.rt.RunString(fmt.Sprintf(` 287 | const redis = new Client('redis://%s'); 288 | 289 | redis.get("existing_key") 290 | .then(res => { if (res !== "old_value") { throw 'unexpected value for get result: ' + res } }) 291 | .then(() => redis.get("non_existing_key")) 292 | .then( 293 | res => { throw 'expected to fail getting non-existing key from redis' }, 294 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error: ' + err } } 295 | ) 296 | `, rs.Addr())) 297 | 298 | return err 299 | }) 300 | 301 | assert.NoError(t, gotScriptErr) 302 | assert.Equal(t, 2, rs.HandledCommandsCount()) 303 | assert.Equal(t, [][]string{ 304 | {"HELLO", "2"}, 305 | {"GET", "existing_key"}, 306 | {"GET", "non_existing_key"}, 307 | }, rs.GotCommands()) 308 | } 309 | 310 | func TestClientGetSet(t *testing.T) { 311 | t.Parallel() 312 | 313 | ts := newTestSetup(t) 314 | rs := RunT(t) 315 | rs.RegisterCommandHandler("GETSET", func(c *Connection, args []string) { 316 | if len(args) != 2 { 317 | c.WriteError(errors.New("ERR unexpected number of arguments for 'GETSET' command")) 318 | return 319 | } 320 | 321 | switch args[0] { 322 | case "existing_key": 323 | c.WriteBulkString("old_value") 324 | case "non_existing_key": 325 | c.WriteOK() 326 | } 327 | }) 328 | 329 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 330 | _, err := ts.rt.RunString(fmt.Sprintf(` 331 | const redis = new Client('redis://%s'); 332 | 333 | redis.getSet("existing_key", "new_value") 334 | .then(res => { if (res !== "old_value") { throw 'unexpected value for getSet result: ' + res } }) 335 | .then(() => redis.getSet("non_existing_key", "some_value")) 336 | .then(res => { if (res !== "OK") { throw 'unexpected value for getSet result: ' + res } }) 337 | .then(() => redis.getSet("unsupported_type", new Array("unsupported"))) 338 | .then( 339 | res => { throw 'unexpectedly resolve getset unsupported type' }, 340 | err => { if (!err.error().startsWith('unsupported type')) { throw 'unexpected error: ' + err } } 341 | ) 342 | `, rs.Addr())) 343 | 344 | return err 345 | }) 346 | 347 | assert.NoError(t, gotScriptErr) 348 | assert.Equal(t, 2, rs.HandledCommandsCount()) 349 | assert.Equal(t, [][]string{ 350 | {"HELLO", "2"}, 351 | {"GETSET", "existing_key", "new_value"}, 352 | {"GETSET", "non_existing_key", "some_value"}, 353 | }, rs.GotCommands()) 354 | } 355 | 356 | func TestClientDel(t *testing.T) { 357 | t.Parallel() 358 | 359 | ts := newTestSetup(t) 360 | rs := RunT(t) 361 | rs.RegisterCommandHandler("DEL", func(c *Connection, args []string) { 362 | if len(args) != 3 { 363 | c.WriteError(errors.New("ERR unexpected number of arguments for 'DEL' command")) 364 | return 365 | } 366 | 367 | c.WriteInteger(2) 368 | }) 369 | 370 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 371 | _, err := ts.rt.RunString(fmt.Sprintf(` 372 | const redis = new Client('redis://%s'); 373 | 374 | redis.del("key1", "key2", "nonexisting_key") 375 | .then(res => { if (res !== 2) { throw 'unexpected value for del result: ' + res } }) 376 | `, rs.Addr())) 377 | 378 | return err 379 | }) 380 | 381 | assert.NoError(t, gotScriptErr) 382 | assert.Equal(t, 1, rs.HandledCommandsCount()) 383 | assert.Equal(t, [][]string{ 384 | {"HELLO", "2"}, 385 | {"DEL", "key1", "key2", "nonexisting_key"}, 386 | }, rs.GotCommands()) 387 | } 388 | 389 | func TestClientGetDel(t *testing.T) { 390 | t.Parallel() 391 | 392 | ts := newTestSetup(t) 393 | rs := RunT(t) 394 | rs.RegisterCommandHandler("GETDEL", func(c *Connection, args []string) { 395 | if len(args) != 1 { 396 | c.WriteError(errors.New("ERR unexpected number of arguments for 'GETDEL' command")) 397 | return 398 | } 399 | 400 | switch args[0] { 401 | case "existing_key": 402 | c.WriteBulkString("old_value") 403 | case "non_existing_key": 404 | c.WriteNull() 405 | } 406 | }) 407 | 408 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 409 | _, err := ts.rt.RunString(fmt.Sprintf(` 410 | const redis = new Client('redis://%s'); 411 | 412 | redis.getDel("existing_key") 413 | .then(res => { if (res !== "old_value") { throw 'unexpected value for getDel result: ' + res } }) 414 | .then(() => redis.getDel("non_existing_key")) 415 | .then( 416 | res => { if (res !== null) { throw 'unexpected value for getSet result: ' + res } }, 417 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error: ' + err } } 418 | ) 419 | `, rs.Addr())) 420 | 421 | return err 422 | }) 423 | 424 | assert.NoError(t, gotScriptErr) 425 | assert.Equal(t, 2, rs.HandledCommandsCount()) 426 | assert.Equal(t, [][]string{ 427 | {"HELLO", "2"}, 428 | {"GETDEL", "existing_key"}, 429 | {"GETDEL", "non_existing_key"}, 430 | }, rs.GotCommands()) 431 | } 432 | 433 | func TestClientExists(t *testing.T) { 434 | t.Parallel() 435 | 436 | ts := newTestSetup(t) 437 | rs := RunT(t) 438 | rs.RegisterCommandHandler("EXISTS", func(c *Connection, args []string) { 439 | if len(args) == 0 { 440 | c.WriteError(errors.New("ERR unexpected number of arguments for 'EXISTS' command")) 441 | return 442 | } 443 | 444 | c.WriteInteger(1) 445 | }) 446 | 447 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 448 | _, err := ts.rt.RunString(fmt.Sprintf(` 449 | const redis = new Client('redis://%s'); 450 | 451 | redis.exists("existing_key", "nonexisting_key") 452 | .then(res => { if (res !== 1) { throw 'unexpected value for exists result: ' + res } }) 453 | `, rs.Addr())) 454 | 455 | return err 456 | }) 457 | 458 | assert.NoError(t, gotScriptErr) 459 | assert.Equal(t, 1, rs.HandledCommandsCount()) 460 | assert.Equal(t, [][]string{ 461 | {"HELLO", "2"}, 462 | {"EXISTS", "existing_key", "nonexisting_key"}, 463 | }, rs.GotCommands()) 464 | } 465 | 466 | func TestClientIncr(t *testing.T) { 467 | t.Parallel() 468 | 469 | ts := newTestSetup(t) 470 | rs := RunT(t) 471 | rs.RegisterCommandHandler("INCR", func(c *Connection, args []string) { 472 | if len(args) != 1 { 473 | c.WriteError(errors.New("ERR unexpected number of arguments for 'INCR' command")) 474 | return 475 | } 476 | 477 | existingValue := 10 478 | 479 | switch args[0] { 480 | case "existing_key": 481 | c.WriteInteger(existingValue + 1) 482 | case "non_existing_key": 483 | c.WriteInteger(0 + 1) 484 | } 485 | }) 486 | 487 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 488 | _, err := ts.rt.RunString(fmt.Sprintf(` 489 | const redis = new Client('redis://%s'); 490 | 491 | redis.incr("existing_key") 492 | .then(res => { if (res !== 11) { throw 'unexpected value for existing key incr result: ' + res } }) 493 | .then(() => redis.incr("non_existing_key")) 494 | .then(res => { if (res !== 1) { throw 'unexpected value for non existing key incr result: ' + res } }) 495 | `, rs.Addr())) 496 | 497 | return err 498 | }) 499 | 500 | assert.NoError(t, gotScriptErr) 501 | assert.Equal(t, 2, rs.HandledCommandsCount()) 502 | assert.Equal(t, [][]string{ 503 | {"HELLO", "2"}, 504 | {"INCR", "existing_key"}, 505 | {"INCR", "non_existing_key"}, 506 | }, rs.GotCommands()) 507 | } 508 | 509 | func TestClientIncrBy(t *testing.T) { 510 | t.Parallel() 511 | 512 | ts := newTestSetup(t) 513 | rs := RunT(t) 514 | rs.RegisterCommandHandler("INCRBY", func(c *Connection, args []string) { 515 | if len(args) != 2 { 516 | c.WriteError(errors.New("ERR unexpected number of arguments for 'INCRBY' command")) 517 | return 518 | } 519 | 520 | value, err := strconv.Atoi(args[1]) 521 | if err != nil { 522 | c.WriteError(err) 523 | return 524 | } 525 | 526 | existingValue := 10 527 | 528 | switch args[0] { 529 | case "existing_key": 530 | c.WriteInteger(existingValue + value) 531 | case "non_existing_key": 532 | c.WriteInteger(0 + value) 533 | } 534 | }) 535 | 536 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 537 | _, err := ts.rt.RunString(fmt.Sprintf(` 538 | const redis = new Client('redis://%s'); 539 | 540 | redis.incrBy("existing_key", 10) 541 | .then(res => { if (res !== 20) { throw 'unexpected value for incrBy result: ' + res } }) 542 | .then(() => redis.incrBy("non_existing_key", 10)) 543 | .then(res => { if (res !== 10) { throw 'unexpected value for incrBy result: ' + res } }) 544 | `, rs.Addr())) 545 | 546 | return err 547 | }) 548 | 549 | assert.NoError(t, gotScriptErr) 550 | assert.Equal(t, 2, rs.HandledCommandsCount()) 551 | assert.Equal(t, [][]string{ 552 | {"HELLO", "2"}, 553 | {"INCRBY", "existing_key", "10"}, 554 | {"INCRBY", "non_existing_key", "10"}, 555 | }, rs.GotCommands()) 556 | } 557 | 558 | func TestClientDecr(t *testing.T) { 559 | t.Parallel() 560 | 561 | ts := newTestSetup(t) 562 | rs := RunT(t) 563 | rs.RegisterCommandHandler("DECR", func(c *Connection, args []string) { 564 | if len(args) != 1 { 565 | c.WriteError(errors.New("ERR unexpected number of arguments for 'DECR' command")) 566 | return 567 | } 568 | 569 | existingValue := 10 570 | 571 | switch args[0] { 572 | case "existing_key": 573 | c.WriteInteger(existingValue - 1) 574 | case "non_existing_key": 575 | c.WriteInteger(0 - 1) 576 | } 577 | }) 578 | 579 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 580 | _, err := ts.rt.RunString(fmt.Sprintf(` 581 | const redis = new Client('redis://%s'); 582 | 583 | redis.decr("existing_key") 584 | .then(res => { if (res !== 9) { throw 'unexpected value for decr result: ' + res } }) 585 | .then(() => redis.decr("non_existing_key")) 586 | .then(res => { if (res !== -1) { throw 'unexpected value for decr result: ' + res } }) 587 | `, rs.Addr())) 588 | 589 | return err 590 | }) 591 | 592 | assert.NoError(t, gotScriptErr) 593 | assert.Equal(t, 2, rs.HandledCommandsCount()) 594 | assert.Equal(t, [][]string{ 595 | {"HELLO", "2"}, 596 | {"DECR", "existing_key"}, 597 | {"DECR", "non_existing_key"}, 598 | }, rs.GotCommands()) 599 | } 600 | 601 | func TestClientDecrBy(t *testing.T) { 602 | t.Parallel() 603 | 604 | ts := newTestSetup(t) 605 | rs := RunT(t) 606 | rs.RegisterCommandHandler("DECRBY", func(c *Connection, args []string) { 607 | if len(args) != 2 { 608 | c.WriteError(errors.New("ERR unexpected number of arguments for 'DECRBY' command")) 609 | return 610 | } 611 | 612 | value, err := strconv.Atoi(args[1]) 613 | if err != nil { 614 | c.WriteError(err) 615 | return 616 | } 617 | 618 | existingValue := 10 619 | 620 | switch args[0] { 621 | case "existing_key": 622 | c.WriteInteger(existingValue - value) 623 | case "non_existing_key": 624 | c.WriteInteger(0 - value) 625 | } 626 | }) 627 | 628 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 629 | _, err := ts.rt.RunString(fmt.Sprintf(` 630 | const redis = new Client('redis://%s'); 631 | 632 | redis.decrBy("existing_key", 2) 633 | .then(res => { if (res !== 8) { throw 'unexpected value for decrBy result: ' + res } }) 634 | .then(() => redis.decrBy("non_existing_key", 2)) 635 | .then(res => { if (res !== -2) { throw 'unexpected value for decrBy result: ' + res } }) 636 | `, rs.Addr())) 637 | 638 | return err 639 | }) 640 | 641 | assert.NoError(t, gotScriptErr) 642 | assert.Equal(t, 2, rs.HandledCommandsCount()) 643 | assert.Equal(t, [][]string{ 644 | {"HELLO", "2"}, 645 | {"DECRBY", "existing_key", "2"}, 646 | {"DECRBY", "non_existing_key", "2"}, 647 | }, rs.GotCommands()) 648 | } 649 | 650 | func TestClientRandomKey(t *testing.T) { 651 | t.Parallel() 652 | 653 | ts := newTestSetup(t) 654 | rs := RunT(t) 655 | calledN := 0 656 | rs.RegisterCommandHandler("RANDOMKEY", func(c *Connection, args []string) { 657 | if len(args) != 0 { 658 | c.WriteError(errors.New("ERR unexpected number of arguments for 'RANDOMKEY' command")) 659 | return 660 | } 661 | 662 | if calledN == 0 { 663 | // let's consider the DB empty 664 | calledN++ 665 | c.WriteNull() 666 | return 667 | } 668 | 669 | c.WriteBulkString("random_key") 670 | }) 671 | 672 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 673 | _, err := ts.rt.RunString(fmt.Sprintf(` 674 | const redis = new Client('redis://%s'); 675 | 676 | redis.randomKey() 677 | .then( 678 | res => { throw 'unexpectedly resolved promise for randomKey command: ' + res }, 679 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error: ' + err } } 680 | ) 681 | .then(() => redis.randomKey()) 682 | .then(res => { if (res !== "random_key") { throw 'unexpected value for randomKey result: ' + res } }) 683 | `, rs.Addr())) 684 | 685 | return err 686 | }) 687 | 688 | assert.NoError(t, gotScriptErr) 689 | assert.Equal(t, 2, rs.HandledCommandsCount()) 690 | assert.Equal(t, [][]string{ 691 | {"HELLO", "2"}, 692 | {"RANDOMKEY"}, 693 | {"RANDOMKEY"}, 694 | }, rs.GotCommands()) 695 | } 696 | 697 | func TestClientMget(t *testing.T) { 698 | t.Parallel() 699 | 700 | ts := newTestSetup(t) 701 | rs := RunT(t) 702 | rs.RegisterCommandHandler("MGET", func(c *Connection, args []string) { 703 | if len(args) < 1 { 704 | c.WriteError(errors.New("ERR unexpected number of arguments for 'MGET' command")) 705 | return 706 | } 707 | 708 | c.WriteArray("old_value", "") 709 | }) 710 | 711 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 712 | _, err := ts.rt.RunString(fmt.Sprintf(` 713 | const redis = new Client('redis://%s'); 714 | 715 | redis.mget("existing_key", "non_existing_key") 716 | .then( 717 | res => { 718 | if (res.length !== 2 || res[0] !== "old_value" || res[1] !== null) { 719 | throw 'unexpected value for mget result: ' + res 720 | } 721 | } 722 | ) 723 | `, rs.Addr())) 724 | 725 | return err 726 | }) 727 | 728 | assert.NoError(t, gotScriptErr) 729 | assert.Equal(t, 1, rs.HandledCommandsCount()) 730 | assert.Equal(t, [][]string{ 731 | {"HELLO", "2"}, 732 | {"MGET", "existing_key", "non_existing_key"}, 733 | }, rs.GotCommands()) 734 | } 735 | 736 | func TestClientExpire(t *testing.T) { 737 | t.Parallel() 738 | 739 | ts := newTestSetup(t) 740 | rs := RunT(t) 741 | rs.RegisterCommandHandler("EXPIRE", func(c *Connection, args []string) { 742 | if len(args) != 2 { 743 | c.WriteError(errors.New("ERR unexpected number of arguments for 'EXPIRE' command")) 744 | return 745 | } 746 | 747 | switch args[0] { 748 | case "expires_key": 749 | c.WriteInteger(1) 750 | case "non_existing_key": 751 | c.WriteInteger(0) 752 | } 753 | }) 754 | 755 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 756 | _, err := ts.rt.RunString(fmt.Sprintf(` 757 | const redis = new Client('redis://%s'); 758 | 759 | redis.expire("expires_key", 10) 760 | .then(res => { if (res !== true) { throw 'unexpected value for expire result: ' + res } }) 761 | .then(() => redis.expire("non_existing_key", 1)) 762 | .then(res => { if (res !== false) { throw 'unexpected value for expire result: ' + res } }) 763 | `, rs.Addr())) 764 | 765 | return err 766 | }) 767 | 768 | assert.NoError(t, gotScriptErr) 769 | assert.Equal(t, 2, rs.HandledCommandsCount()) 770 | assert.Equal(t, [][]string{ 771 | {"HELLO", "2"}, 772 | {"EXPIRE", "expires_key", "10"}, 773 | {"EXPIRE", "non_existing_key", "1"}, 774 | }, rs.GotCommands()) 775 | } 776 | 777 | func TestClientTTL(t *testing.T) { 778 | t.Parallel() 779 | 780 | ts := newTestSetup(t) 781 | rs := RunT(t) 782 | rs.RegisterCommandHandler("TTL", func(c *Connection, args []string) { 783 | if len(args) != 1 { 784 | c.WriteError(errors.New("ERR unexpected number of arguments for 'EXPIRE' command")) 785 | return 786 | } 787 | 788 | switch args[0] { 789 | case "expires_key": 790 | c.WriteInteger(10) 791 | case "non_existing_key": 792 | c.WriteInteger(0) 793 | } 794 | }) 795 | 796 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 797 | _, err := ts.rt.RunString(fmt.Sprintf(` 798 | const redis = new Client('redis://%s'); 799 | 800 | redis.ttl("expires_key") 801 | .then(res => { if (res !== 10) { throw 'unexpected value for expire result: ' + res } }) 802 | .then(() => redis.ttl("non_existing_key")) 803 | .then(res => { if (res > 0) { throw 'unexpected value for expire result: ' + res } }) 804 | `, rs.Addr())) 805 | 806 | return err 807 | }) 808 | 809 | assert.NoError(t, gotScriptErr) 810 | assert.Equal(t, 2, rs.HandledCommandsCount()) 811 | assert.Equal(t, [][]string{ 812 | {"HELLO", "2"}, 813 | {"TTL", "expires_key"}, 814 | {"TTL", "non_existing_key"}, 815 | }, rs.GotCommands()) 816 | } 817 | 818 | func TestClientPersist(t *testing.T) { 819 | t.Parallel() 820 | 821 | ts := newTestSetup(t) 822 | rs := RunT(t) 823 | rs.RegisterCommandHandler("PERSIST", func(c *Connection, args []string) { 824 | if len(args) != 1 { 825 | c.WriteError(errors.New("ERR unexpected number of arguments for 'PERSIST' command")) 826 | return 827 | } 828 | 829 | switch args[0] { 830 | case "expires_key": 831 | c.WriteInteger(1) 832 | case "non_existing_key": 833 | c.WriteInteger(0) 834 | } 835 | }) 836 | 837 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 838 | _, err := ts.rt.RunString(fmt.Sprintf(` 839 | const redis = new Client('redis://%s'); 840 | 841 | redis.persist("expires_key") 842 | .then(res => { if (res !== true) { throw 'unexpected value for expire result: ' + res } }) 843 | .then(() => redis.persist("non_existing_key")) 844 | .then(res => { if (res !== false) { throw 'unexpected value for expire result: ' + res } }) 845 | `, rs.Addr())) 846 | 847 | return err 848 | }) 849 | 850 | assert.NoError(t, gotScriptErr) 851 | assert.Equal(t, 2, rs.HandledCommandsCount()) 852 | assert.Equal(t, [][]string{ 853 | {"HELLO", "2"}, 854 | {"PERSIST", "expires_key"}, 855 | {"PERSIST", "non_existing_key"}, 856 | }, rs.GotCommands()) 857 | } 858 | 859 | func TestClientLPush(t *testing.T) { 860 | t.Parallel() 861 | 862 | ts := newTestSetup(t) 863 | rs := RunT(t) 864 | rs.RegisterCommandHandler("LPUSH", func(c *Connection, args []string) { 865 | if len(args) < 2 { 866 | c.WriteError(errors.New("ERR unexpected number of arguments for 'LPUSH' command")) 867 | return 868 | } 869 | 870 | existingList := []string{"existing_key"} 871 | 872 | switch args[0] { 873 | case "existing_list": //nolint:goconst 874 | existingList = append(args[1:], existingList...) 875 | c.WriteInteger(len(existingList)) 876 | case "new_list": 877 | c.WriteInteger(1) 878 | } 879 | }) 880 | 881 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 882 | _, err := ts.rt.RunString(fmt.Sprintf(` 883 | const redis = new Client('redis://%s'); 884 | 885 | redis.lpush("existing_list", "second", "first") 886 | .then(res => { if (res !== 3) { throw 'unexpected value for lpush result: ' + res } }) 887 | .then(() => redis.lpush("new_list", 1)) 888 | .then(res => { if (res !== 1) { throw 'unexpected value for lpush result: ' + res } }) 889 | `, rs.Addr())) 890 | 891 | return err 892 | }) 893 | 894 | assert.NoError(t, gotScriptErr) 895 | assert.Equal(t, 2, rs.HandledCommandsCount()) 896 | assert.Equal(t, [][]string{ 897 | {"HELLO", "2"}, 898 | {"LPUSH", "existing_list", "second", "first"}, 899 | {"LPUSH", "new_list", "1"}, 900 | }, rs.GotCommands()) 901 | } 902 | 903 | func TestClientRPush(t *testing.T) { 904 | t.Parallel() 905 | 906 | ts := newTestSetup(t) 907 | rs := RunT(t) 908 | rs.RegisterCommandHandler("RPUSH", func(c *Connection, args []string) { 909 | if len(args) < 2 { 910 | c.WriteError(errors.New("ERR unexpected number of arguments for 'RPUSH' command")) 911 | return 912 | } 913 | 914 | existingList := []string{"existing_key"} 915 | 916 | switch args[0] { 917 | case "existing_list": 918 | existingList = append(existingList, args[1:]...) 919 | c.WriteInteger(len(existingList)) 920 | case "new_list": 921 | c.WriteInteger(1) 922 | } 923 | }) 924 | 925 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 926 | _, err := ts.rt.RunString(fmt.Sprintf(` 927 | const redis = new Client('redis://%s'); 928 | 929 | redis.rpush("existing_list", "second", "third") 930 | .then(res => { if (res !== 3) { throw 'unexpected value for rpush result: ' + res } }) 931 | .then(() => redis.rpush("new_list", 1)) 932 | .then(res => { if (res !== 1) { throw 'unexpected value for rpush result: ' + res } }) 933 | `, rs.Addr())) 934 | 935 | return err 936 | }) 937 | 938 | assert.NoError(t, gotScriptErr) 939 | assert.Equal(t, 2, rs.HandledCommandsCount()) 940 | assert.Equal(t, [][]string{ 941 | {"HELLO", "2"}, 942 | {"RPUSH", "existing_list", "second", "third"}, 943 | {"RPUSH", "new_list", "1"}, 944 | }, rs.GotCommands()) 945 | } 946 | 947 | func TestClientLPop(t *testing.T) { 948 | t.Parallel() 949 | 950 | ts := newTestSetup(t) 951 | rs := RunT(t) 952 | listState := []string{"first", "second"} 953 | rs.RegisterCommandHandler("LPOP", func(c *Connection, args []string) { 954 | if len(args) != 1 { 955 | c.WriteError(errors.New("ERR unexpected number of arguments for 'LPOP' command")) 956 | return 957 | } 958 | 959 | switch args[0] { 960 | case "existing_list": 961 | c.WriteBulkString(listState[0]) 962 | listState = listState[1:] 963 | case "non_existing_list": //nolint:goconst 964 | c.WriteNull() 965 | } 966 | }) 967 | 968 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 969 | _, err := ts.rt.RunString(fmt.Sprintf(` 970 | const redis = new Client('redis://%s'); 971 | 972 | redis.lpop("existing_list") 973 | .then(res => { if (res !== "first") { throw 'unexpected value for lpop first result: ' + res } }) 974 | .then(() => redis.lpop("existing_list")) 975 | .then(res => { if (res !== "second") { throw 'unexpected value for lpop second result: ' + res } }) 976 | .then(() => redis.lpop("non_existing_list")) 977 | .then( 978 | res => { if (res !== null) { throw 'unexpectedly resolved lpop promise: ' + res } }, 979 | 980 | // An error is returned if the list does not exist 981 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error for lpop: ' + err.error() } } 982 | ) 983 | `, rs.Addr())) 984 | 985 | return err 986 | }) 987 | 988 | assert.NoError(t, gotScriptErr) 989 | assert.Equal(t, 3, rs.HandledCommandsCount()) 990 | assert.Equal(t, [][]string{ 991 | {"HELLO", "2"}, 992 | {"LPOP", "existing_list"}, 993 | {"LPOP", "existing_list"}, 994 | {"LPOP", "non_existing_list"}, 995 | }, rs.GotCommands()) 996 | } 997 | 998 | func TestClientRPop(t *testing.T) { 999 | t.Parallel() 1000 | 1001 | ts := newTestSetup(t) 1002 | rs := RunT(t) 1003 | listState := []string{"first", "second"} 1004 | rs.RegisterCommandHandler("RPOP", func(c *Connection, args []string) { 1005 | if len(args) != 1 { 1006 | c.WriteError(errors.New("ERR unexpected number of arguments for 'RPOP' command")) 1007 | return 1008 | } 1009 | 1010 | switch args[0] { 1011 | case "existing_list": 1012 | c.WriteBulkString(listState[len(listState)-1]) 1013 | listState = listState[:len(listState)-1] 1014 | case "non_existing_list": 1015 | c.WriteNull() 1016 | } 1017 | }) 1018 | 1019 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1020 | _, err := ts.rt.RunString(fmt.Sprintf(` 1021 | const redis = new Client('redis://%s'); 1022 | 1023 | redis.rpop("existing_list") 1024 | .then(res => { if (res !== "second") { throw 'unexpected value for rpop result: ' + res }}) 1025 | .then(() => redis.rpop("existing_list")) 1026 | .then(res => { if (res !== "first") { throw 'unexpected value for rpop result: ' + res }}) 1027 | .then(() => redis.rpop("non_existing_list")) 1028 | .then( 1029 | res => { if (res !== null) { throw 'unexpectedly resolved lpop promise: ' + res } }, 1030 | 1031 | // An error is returned if the list does not exist 1032 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error for rpop: ' + err.error() } } 1033 | ) 1034 | `, rs.Addr())) 1035 | 1036 | return err 1037 | }) 1038 | 1039 | assert.NoError(t, gotScriptErr) 1040 | assert.Equal(t, 3, rs.HandledCommandsCount()) 1041 | assert.Equal(t, [][]string{ 1042 | {"HELLO", "2"}, 1043 | {"RPOP", "existing_list"}, 1044 | {"RPOP", "existing_list"}, 1045 | {"RPOP", "non_existing_list"}, 1046 | }, rs.GotCommands()) 1047 | } 1048 | 1049 | func TestClientLRange(t *testing.T) { 1050 | t.Parallel() 1051 | 1052 | ts := newTestSetup(t) 1053 | rs := RunT(t) 1054 | listState := []string{"first", "second", "third"} 1055 | rs.RegisterCommandHandler("LRANGE", func(c *Connection, args []string) { 1056 | if len(args) != 3 { 1057 | c.WriteError(errors.New("ERR unexpected number of arguments for 'LRANGE' command")) 1058 | return 1059 | } 1060 | 1061 | start, err := strconv.Atoi(args[1]) 1062 | if err != nil { 1063 | c.WriteError(err) 1064 | return 1065 | } 1066 | 1067 | stop, err := strconv.Atoi(args[2]) 1068 | if err != nil { 1069 | c.WriteError(err) 1070 | return 1071 | } 1072 | 1073 | if start < 0 { 1074 | start = len(listState) + start 1075 | } 1076 | 1077 | // This calculation is done in a way that is not 100% correct, but it is 1078 | // good enough for the test. 1079 | c.WriteArray(listState[start : stop+1]...) 1080 | }) 1081 | 1082 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1083 | _, err := ts.rt.RunString(fmt.Sprintf(` 1084 | const redis = new Client('redis://%s'); 1085 | 1086 | redis.lrange("existing_list", 0, 0) 1087 | .then(res => { if (res.length !== 1 || res[0] !== "first") { throw 'unexpected value for lrange result: ' + res }}) 1088 | .then(() => redis.lrange("existing_list", 0, 1)) 1089 | .then(res => { if (res.length !== 2 || res[0] !== "first" || res[1] !== "second") { throw 'unexpected value for lrange result: ' + res } }) 1090 | .then(() => redis.lrange("existing_list", -2, 2)) 1091 | .then(res => { 1092 | if (res.length !== 2 || 1093 | res[0] !== "second" || 1094 | res[1] !== "third") { 1095 | throw 'unexpected value for lrange result: ' + res 1096 | } 1097 | }) 1098 | `, rs.Addr())) 1099 | 1100 | return err 1101 | }) 1102 | 1103 | assert.NoError(t, gotScriptErr) 1104 | assert.Equal(t, 3, rs.HandledCommandsCount()) 1105 | assert.Equal(t, [][]string{ 1106 | {"HELLO", "2"}, 1107 | {"LRANGE", "existing_list", "0", "0"}, 1108 | {"LRANGE", "existing_list", "0", "1"}, 1109 | {"LRANGE", "existing_list", "-2", "2"}, 1110 | }, rs.GotCommands()) 1111 | } 1112 | 1113 | func TestClientLIndex(t *testing.T) { 1114 | t.Parallel() 1115 | 1116 | ts := newTestSetup(t) 1117 | rs := RunT(t) 1118 | listState := []string{"first", "second", "third"} 1119 | rs.RegisterCommandHandler("LINDEX", func(c *Connection, args []string) { 1120 | if len(args) != 2 { 1121 | c.WriteError(errors.New("ERR unexpected number of arguments for 'LINDEX' command")) 1122 | return 1123 | } 1124 | 1125 | if args[0] == "non_existing_list" { 1126 | c.WriteNull() 1127 | return 1128 | } 1129 | 1130 | index, err := strconv.Atoi(args[1]) 1131 | if err != nil { 1132 | c.WriteError(err) 1133 | return 1134 | } 1135 | 1136 | if index > len(listState)-1 { 1137 | c.WriteNull() 1138 | return 1139 | } 1140 | 1141 | // This calculation is done in a way that is not 100% correct, but it is 1142 | // good enough for the test. 1143 | c.WriteBulkString(listState[index]) 1144 | }) 1145 | 1146 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1147 | _, err := ts.rt.RunString(fmt.Sprintf(` 1148 | const redis = new Client('redis://%s'); 1149 | 1150 | redis.lindex("existing_list", 0) 1151 | .then(res => { if (res !== "first") { throw 'unexpected value for lindex result: ' + res } }) 1152 | .then(() => redis.lindex("existing_list", 3)) 1153 | .then( 1154 | res => { throw 'unexpectedly resolved lindex command promise: ' + res }, 1155 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error for lindex: ' + err.error() } } 1156 | ) 1157 | .then(() => redis.lindex("non_existing_list", 0)) 1158 | .then( 1159 | res => { throw 'unexpectedly resolved lindex command promise: ' + res }, 1160 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error for lindex: ' + err.error() } } 1161 | ) 1162 | `, rs.Addr())) 1163 | 1164 | return err 1165 | }) 1166 | 1167 | assert.NoError(t, gotScriptErr) 1168 | assert.Equal(t, 3, rs.HandledCommandsCount()) 1169 | assert.Equal(t, [][]string{ 1170 | {"HELLO", "2"}, 1171 | {"LINDEX", "existing_list", "0"}, 1172 | {"LINDEX", "existing_list", "3"}, 1173 | {"LINDEX", "non_existing_list", "0"}, 1174 | }, rs.GotCommands()) 1175 | } 1176 | 1177 | func TestClientClientLSet(t *testing.T) { 1178 | t.Parallel() 1179 | 1180 | ts := newTestSetup(t) 1181 | rs := RunT(t) 1182 | listState := []string{"first"} 1183 | rs.RegisterCommandHandler("LSET", func(c *Connection, args []string) { 1184 | if len(args) != 3 { 1185 | c.WriteError(errors.New("ERR unexpected number of arguments for 'LSET' command")) 1186 | return 1187 | } 1188 | 1189 | if args[0] == "non_existing_list" { 1190 | c.WriteError(errors.New("ERR no such key")) 1191 | return 1192 | } 1193 | 1194 | index, err := strconv.Atoi(args[1]) 1195 | if err != nil { 1196 | c.WriteError(err) 1197 | return 1198 | } 1199 | 1200 | listState[index] = args[2] 1201 | c.WriteOK() 1202 | }) 1203 | 1204 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1205 | _, err := ts.rt.RunString(fmt.Sprintf(` 1206 | const redis = new Client('redis://%s'); 1207 | 1208 | redis.lset("existing_list", 0, "new_first") 1209 | .then(res => { if (res !== "OK") { throw 'unexpected value for lset result: ' + res }}) 1210 | .then(() => redis.lset("existing_list", 0, "overridden_value")) 1211 | .then(() => redis.lset("non_existing_list", 0, "new_first")) 1212 | .then( 1213 | res => { if (res !== null) { throw 'unexpectedly resolved promise: ' + res } }, 1214 | err => { if (err.error() != 'ERR no such key') { throw 'unexpected error for lset: ' + err.error() } } 1215 | ) 1216 | `, rs.Addr())) 1217 | 1218 | return err 1219 | }) 1220 | 1221 | assert.NoError(t, gotScriptErr) 1222 | assert.Equal(t, 3, rs.HandledCommandsCount()) 1223 | assert.Equal(t, [][]string{ 1224 | {"HELLO", "2"}, 1225 | {"LSET", "existing_list", "0", "new_first"}, 1226 | {"LSET", "existing_list", "0", "overridden_value"}, 1227 | {"LSET", "non_existing_list", "0", "new_first"}, 1228 | }, rs.GotCommands()) 1229 | } 1230 | 1231 | func TestClientLrem(t *testing.T) { 1232 | t.Parallel() 1233 | 1234 | ts := newTestSetup(t) 1235 | rs := RunT(t) 1236 | rs.RegisterCommandHandler("LREM", func(c *Connection, args []string) { 1237 | if len(args) != 3 { 1238 | c.WriteError(errors.New("ERR unexpected number of arguments for 'LREM' command")) 1239 | return 1240 | } 1241 | 1242 | if args[0] == "non_existing_list" { 1243 | c.WriteError(errors.New("ERR no such key")) 1244 | return 1245 | } 1246 | 1247 | c.WriteInteger(1) 1248 | }) 1249 | 1250 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1251 | _, err := ts.rt.RunString(fmt.Sprintf(` 1252 | const redis = new Client('redis://%s'); 1253 | 1254 | redis.lrem("existing_list", 1, "first") 1255 | .then(() => redis.lrem("existing_list", 0, "second")) 1256 | .then(() => { 1257 | redis.lrem("non_existing_list", 2, "third") 1258 | .then( 1259 | res => { if (res !== null) { throw 'unexpectedly resolved promise: ' + res } }, 1260 | err => { if (err.error() != 'ERR no such key') { throw 'unexpected error for lrem: ' + err.error() } }, 1261 | ) 1262 | }) 1263 | `, rs.Addr())) 1264 | 1265 | return err 1266 | }) 1267 | 1268 | assert.NoError(t, gotScriptErr) 1269 | assert.Equal(t, 3, rs.HandledCommandsCount()) 1270 | assert.Equal(t, [][]string{ 1271 | {"HELLO", "2"}, 1272 | {"LREM", "existing_list", "1", "first"}, 1273 | {"LREM", "existing_list", "0", "second"}, 1274 | {"LREM", "non_existing_list", "2", "third"}, 1275 | }, rs.GotCommands()) 1276 | } 1277 | 1278 | func TestClientLlen(t *testing.T) { 1279 | t.Parallel() 1280 | 1281 | ts := newTestSetup(t) 1282 | rs := RunT(t) 1283 | rs.RegisterCommandHandler("LLEN", func(c *Connection, args []string) { 1284 | if len(args) != 1 { 1285 | c.WriteError(errors.New("ERR unexpected number of arguments for 'LREM' command")) 1286 | return 1287 | } 1288 | 1289 | if args[0] == "non_existing_list" { 1290 | c.WriteError(errors.New("ERR no such key")) 1291 | return 1292 | } 1293 | 1294 | c.WriteInteger(3) 1295 | }) 1296 | 1297 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1298 | _, err := ts.rt.RunString(fmt.Sprintf(` 1299 | const redis = new Client('redis://%s'); 1300 | 1301 | redis.llen("existing_list") 1302 | .then(res => { if (res !== 3) { throw 'unexpected value for llen result: ' + res } }) 1303 | .then(() => { 1304 | redis.llen("non_existing_list") 1305 | .then( 1306 | res => { if (res !== null) { throw 'unexpectedly resolved promise: ' + res } }, 1307 | err => { if (err.error() != 'ERR no such key') { throw 'unexpected error for llen: ' + err.error() } } 1308 | ) 1309 | }) 1310 | `, rs.Addr())) 1311 | 1312 | return err 1313 | }) 1314 | 1315 | assert.NoError(t, gotScriptErr) 1316 | assert.Equal(t, 2, rs.HandledCommandsCount()) 1317 | assert.Equal(t, [][]string{ 1318 | {"HELLO", "2"}, 1319 | {"LLEN", "existing_list"}, 1320 | {"LLEN", "non_existing_list"}, 1321 | }, rs.GotCommands()) 1322 | } 1323 | 1324 | func TestClientHSet(t *testing.T) { 1325 | t.Parallel() 1326 | 1327 | ts := newTestSetup(t) 1328 | rs := RunT(t) 1329 | rs.RegisterCommandHandler("HSET", func(c *Connection, args []string) { 1330 | if len(args) != 3 { 1331 | c.WriteError(errors.New("ERR unexpected number of arguments for 'LREM' command")) 1332 | return 1333 | } 1334 | 1335 | if args[0] == "non_existing_hash" { //nolint:goconst 1336 | c.WriteError(errors.New("ERR no such key")) 1337 | return 1338 | } 1339 | 1340 | c.WriteInteger(1) 1341 | }) 1342 | 1343 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1344 | _, err := ts.rt.RunString(fmt.Sprintf(` 1345 | const redis = new Client('redis://%s'); 1346 | 1347 | redis.hset("existing_hash", "key", "value") 1348 | .then(res => { if (res !== 1) { throw 'unexpected value for hset result: ' + res } }) 1349 | .then(() => redis.hset("existing_hash", "fou", "barre")) 1350 | .then(res => { if (res !== 1) { throw 'unexpected value for hset result: ' + res } }) 1351 | .then(() => redis.hset("non_existing_hash", "cle", "valeur")) 1352 | .then( 1353 | res => { if (res !== null) { throw 'unexpectedly resolved promise: ' + res } }, 1354 | err => { if (err.error() != 'ERR no such key') { throw 'unexpected error for hset: ' + err.error() } }, 1355 | ) 1356 | `, rs.Addr())) 1357 | 1358 | return err 1359 | }) 1360 | 1361 | assert.NoError(t, gotScriptErr) 1362 | assert.Equal(t, 3, rs.HandledCommandsCount()) 1363 | assert.Equal(t, [][]string{ 1364 | {"HELLO", "2"}, 1365 | {"HSET", "existing_hash", "key", "value"}, 1366 | {"HSET", "existing_hash", "fou", "barre"}, 1367 | {"HSET", "non_existing_hash", "cle", "valeur"}, 1368 | }, rs.GotCommands()) 1369 | } 1370 | 1371 | func TestClientHsetnx(t *testing.T) { 1372 | t.Parallel() 1373 | 1374 | ts := newTestSetup(t) 1375 | rs := RunT(t) 1376 | rs.RegisterCommandHandler("HSETNX", func(c *Connection, args []string) { 1377 | if len(args) != 3 { 1378 | c.WriteError(errors.New("ERR unexpected number of arguments for 'HSETNX' command")) 1379 | return 1380 | } 1381 | 1382 | if args[0] == "non_existing_hash" { 1383 | c.WriteInteger(1) // HSET on a non existing hash creates it 1384 | return 1385 | } 1386 | 1387 | // key does not exist 1388 | if args[1] == "key" { 1389 | c.WriteInteger(1) 1390 | return 1391 | } 1392 | 1393 | // key already exists 1394 | c.WriteInteger(0) 1395 | }) 1396 | 1397 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1398 | _, err := ts.rt.RunString(fmt.Sprintf(` 1399 | const redis = new Client('redis://%s'); 1400 | 1401 | redis.hsetnx("existing_hash", "key", "value") 1402 | .then(res => { if (res !== true) { throw 'unexpected value for hsetnx result: ' + res } }) 1403 | .then(() => redis.hsetnx("existing_hash", "foo", "barre")) 1404 | .then(res => { if (res !== false) { throw 'unexpected value for hsetnx result: ' + res } }) 1405 | .then(() => redis.hsetnx("non_existing_hash", "key", "value")) 1406 | .then(res => { if (res !== true) { throw 'unexpected value for hsetnx result: ' + res } }) 1407 | `, rs.Addr())) 1408 | 1409 | return err 1410 | }) 1411 | 1412 | assert.NoError(t, gotScriptErr) 1413 | assert.Equal(t, 3, rs.HandledCommandsCount()) 1414 | assert.Equal(t, [][]string{ 1415 | {"HELLO", "2"}, 1416 | {"HSETNX", "existing_hash", "key", "value"}, 1417 | {"HSETNX", "existing_hash", "foo", "barre"}, 1418 | {"HSETNX", "non_existing_hash", "key", "value"}, 1419 | }, rs.GotCommands()) 1420 | } 1421 | 1422 | func TestClientHget(t *testing.T) { 1423 | t.Parallel() 1424 | 1425 | ts := newTestSetup(t) 1426 | rs := RunT(t) 1427 | rs.RegisterCommandHandler("HGET", func(c *Connection, args []string) { 1428 | if len(args) != 2 { 1429 | c.WriteError(errors.New("ERR unexpected number of arguments for 'HGET' command")) 1430 | return 1431 | } 1432 | 1433 | if args[0] == "non_existing_hash" { 1434 | c.WriteNull() 1435 | return 1436 | } 1437 | 1438 | c.WriteBulkString("bar") 1439 | }) 1440 | 1441 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1442 | _, err := ts.rt.RunString(fmt.Sprintf(` 1443 | const redis = new Client('redis://%s'); 1444 | 1445 | redis.hget("existing_hash", "foo") 1446 | .then(res => { if (res !== "bar") { throw 'unexpected value for hget result: ' + res } }) 1447 | .then(() => redis.hget("non_existing_hash", "key")) 1448 | .then( 1449 | res => { throw 'unexpectedly resolved hget promise : ' + res }, 1450 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error for hget: ' + err.error() } }, 1451 | ) 1452 | `, rs.Addr())) 1453 | 1454 | return err 1455 | }) 1456 | 1457 | assert.NoError(t, gotScriptErr) 1458 | assert.Equal(t, 2, rs.HandledCommandsCount()) 1459 | assert.Equal(t, [][]string{ 1460 | {"HELLO", "2"}, 1461 | {"HGET", "existing_hash", "foo"}, 1462 | {"HGET", "non_existing_hash", "key"}, 1463 | }, rs.GotCommands()) 1464 | } 1465 | 1466 | func TestClientHdel(t *testing.T) { 1467 | t.Parallel() 1468 | 1469 | ts := newTestSetup(t) 1470 | rs := RunT(t) 1471 | rs.RegisterCommandHandler("HDEL", func(c *Connection, args []string) { 1472 | if len(args) != 2 { 1473 | c.WriteError(errors.New("ERR unexpected number of arguments for 'HDEL' command")) 1474 | return 1475 | } 1476 | 1477 | if args[0] == "non_existing_hash" || args[1] == "non_existing_key" { 1478 | c.WriteInteger(0) 1479 | return 1480 | } 1481 | 1482 | c.WriteInteger(1) 1483 | }) 1484 | 1485 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1486 | _, err := ts.rt.RunString(fmt.Sprintf(` 1487 | const redis = new Client('redis://%s'); 1488 | 1489 | redis.hdel("existing_hash", "foo") 1490 | .then(res => { if (res !== 1) { throw 'unexpected value for hdel result: ' + res } }) 1491 | .then(() => redis.hdel("existing_hash", "non_existing_key")) 1492 | .then(res => { if (res !== 0) { throw 'unexpected value for hdel result: ' + res } }) 1493 | .then(() => redis.hdel("non_existing_hash", "key")) 1494 | .then(res => { if (res !== 0) { throw 'unexpected value for hdel result: ' + res } }) 1495 | `, rs.Addr())) 1496 | 1497 | return err 1498 | }) 1499 | 1500 | assert.NoError(t, gotScriptErr) 1501 | assert.Equal(t, 3, rs.HandledCommandsCount()) 1502 | assert.Equal(t, [][]string{ 1503 | {"HELLO", "2"}, 1504 | {"HDEL", "existing_hash", "foo"}, 1505 | {"HDEL", "existing_hash", "non_existing_key"}, 1506 | {"HDEL", "non_existing_hash", "key"}, 1507 | }, rs.GotCommands()) 1508 | } 1509 | 1510 | func TestClientHgetall(t *testing.T) { 1511 | t.Parallel() 1512 | 1513 | ts := newTestSetup(t) 1514 | rs := RunT(t) 1515 | rs.RegisterCommandHandler("HGETALL", func(c *Connection, args []string) { 1516 | if len(args) != 1 { 1517 | c.WriteError(errors.New("ERR unexpected number of arguments for 'HGETALL' command")) 1518 | return 1519 | } 1520 | 1521 | if args[0] == "non_existing_hash" { 1522 | c.WriteArray() 1523 | return 1524 | } 1525 | 1526 | c.WriteArray("foo", "bar") 1527 | }) 1528 | 1529 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1530 | _, err := ts.rt.RunString(fmt.Sprintf(` 1531 | const redis = new Client('redis://%s'); 1532 | 1533 | redis.hgetall("existing_hash") 1534 | .then(res => { if (typeof res !== "object" || res['foo'] !== 'bar') { throw 'unexpected value for hgetall result: ' + res } }) 1535 | .then(() => redis.hgetall("non_existing_hash")) 1536 | .then( 1537 | res => { if (Object.keys(res).length !== 0) { throw 'unexpected value for hgetall result: ' + res} }, 1538 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error for hgetall: ' + err.error() } }, 1539 | ) 1540 | `, rs.Addr())) 1541 | 1542 | return err 1543 | }) 1544 | 1545 | assert.NoError(t, gotScriptErr) 1546 | assert.Equal(t, 2, rs.HandledCommandsCount()) 1547 | assert.Equal(t, [][]string{ 1548 | {"HELLO", "2"}, 1549 | {"HGETALL", "existing_hash"}, 1550 | {"HGETALL", "non_existing_hash"}, 1551 | }, rs.GotCommands()) 1552 | } 1553 | 1554 | func TestClientHkeys(t *testing.T) { 1555 | t.Parallel() 1556 | 1557 | ts := newTestSetup(t) 1558 | rs := RunT(t) 1559 | rs.RegisterCommandHandler("HKEYS", func(c *Connection, args []string) { 1560 | if len(args) != 1 { 1561 | c.WriteError(errors.New("ERR unexpected number of arguments for 'HKEYS' command")) 1562 | return 1563 | } 1564 | 1565 | if args[0] == "non_existing_hash" { 1566 | c.WriteArray() 1567 | return 1568 | } 1569 | 1570 | c.WriteArray("foo") 1571 | }) 1572 | 1573 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1574 | _, err := ts.rt.RunString(fmt.Sprintf(` 1575 | const redis = new Client('redis://%s'); 1576 | 1577 | redis.hkeys("existing_hash") 1578 | .then(res => { if (res.length !== 1 || res[0] !== 'foo') { throw 'unexpected value for hkeys result: ' + res } }) 1579 | .then(() => redis.hkeys("non_existing_hash")) 1580 | .then( 1581 | res => { if (res.length !== 0) { throw 'unexpected value for hkeys result: ' + res} }, 1582 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error for hkeys: ' + err.error() } }, 1583 | ) 1584 | `, rs.Addr())) 1585 | 1586 | return err 1587 | }) 1588 | 1589 | assert.NoError(t, gotScriptErr) 1590 | assert.Equal(t, 2, rs.HandledCommandsCount()) 1591 | assert.Equal(t, [][]string{ 1592 | {"HELLO", "2"}, 1593 | {"HKEYS", "existing_hash"}, 1594 | {"HKEYS", "non_existing_hash"}, 1595 | }, rs.GotCommands()) 1596 | } 1597 | 1598 | func TestClientHvals(t *testing.T) { 1599 | t.Parallel() 1600 | 1601 | ts := newTestSetup(t) 1602 | rs := RunT(t) 1603 | rs.RegisterCommandHandler("HVALS", func(c *Connection, args []string) { 1604 | if len(args) != 1 { 1605 | c.WriteError(errors.New("ERR unexpected number of arguments for 'HVALS' command")) 1606 | return 1607 | } 1608 | 1609 | if args[0] == "non_existing_hash" { 1610 | c.WriteArray() 1611 | return 1612 | } 1613 | 1614 | c.WriteArray("bar") 1615 | }) 1616 | 1617 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1618 | _, err := ts.rt.RunString(fmt.Sprintf(` 1619 | const redis = new Client('redis://%s'); 1620 | 1621 | redis.hvals("existing_hash") 1622 | .then(res => { if (res.length !== 1 || res[0] !== 'bar') { throw 'unexpected value for hvals result: ' + res } }) 1623 | .then(() => redis.hvals("non_existing_hash")) 1624 | .then( 1625 | res => { if (res.length !== 0) { throw 'unexpected value for hvals result: ' + res} }, 1626 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error for hvals: ' + err.error() } }, 1627 | ) 1628 | `, rs.Addr())) 1629 | 1630 | return err 1631 | }) 1632 | 1633 | assert.NoError(t, gotScriptErr) 1634 | assert.Equal(t, 2, rs.HandledCommandsCount()) 1635 | assert.Equal(t, [][]string{ 1636 | {"HELLO", "2"}, 1637 | {"HVALS", "existing_hash"}, 1638 | {"HVALS", "non_existing_hash"}, 1639 | }, rs.GotCommands()) 1640 | } 1641 | 1642 | func TestClientHlen(t *testing.T) { 1643 | t.Parallel() 1644 | 1645 | ts := newTestSetup(t) 1646 | rs := RunT(t) 1647 | rs.RegisterCommandHandler("HLEN", func(c *Connection, args []string) { 1648 | if len(args) != 1 { 1649 | c.WriteError(errors.New("ERR unexpected number of arguments for 'HLEN' command")) 1650 | return 1651 | } 1652 | 1653 | if args[0] == "non_existing_hash" { 1654 | c.WriteInteger(0) 1655 | return 1656 | } 1657 | 1658 | c.WriteInteger(1) 1659 | }) 1660 | 1661 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1662 | _, err := ts.rt.RunString(fmt.Sprintf(` 1663 | const redis = new Client('redis://%s'); 1664 | 1665 | redis.hlen("existing_hash") 1666 | .then(res => { if (res !== 1) { throw 'unexpected value for hlen result: ' + res } }) 1667 | .then(() => redis.hlen("non_existing_hash")) 1668 | .then( 1669 | res => { if (res !== 0) { throw 'unexpected value for hlen result: ' + res} }, 1670 | err => { if (err.error() != 'redis: nil') { throw 'unexpected error for hlen: ' + err.error() } }, 1671 | ) 1672 | `, rs.Addr())) 1673 | 1674 | return err 1675 | }) 1676 | 1677 | assert.NoError(t, gotScriptErr) 1678 | assert.Equal(t, 2, rs.HandledCommandsCount()) 1679 | assert.Equal(t, [][]string{ 1680 | {"HELLO", "2"}, 1681 | {"HLEN", "existing_hash"}, 1682 | {"HLEN", "non_existing_hash"}, 1683 | }, rs.GotCommands()) 1684 | } 1685 | 1686 | func TestClientHincrby(t *testing.T) { 1687 | t.Parallel() 1688 | 1689 | ts := newTestSetup(t) 1690 | rs := RunT(t) 1691 | fooHValue := 1 1692 | rs.RegisterCommandHandler("HINCRBY", func(c *Connection, args []string) { 1693 | if len(args) != 3 { 1694 | c.WriteError(errors.New("ERR unexpected number of arguments for 'HINCRBY' command")) 1695 | return 1696 | } 1697 | 1698 | if args[0] == "non_existing_hash" { 1699 | c.WriteInteger(1) 1700 | return 1701 | } 1702 | 1703 | value, err := strconv.Atoi(args[2]) 1704 | if err != nil { 1705 | c.WriteError(err) 1706 | return 1707 | } 1708 | 1709 | fooHValue += value 1710 | 1711 | c.WriteInteger(fooHValue) 1712 | }) 1713 | 1714 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1715 | _, err := ts.rt.RunString(fmt.Sprintf(` 1716 | const redis = new Client('redis://%s'); 1717 | 1718 | redis.hincrby("existing_hash", "foo", 1) 1719 | .then(res => { if (res !== 2) { throw 'unexpected value for hincrby result: ' + res } }) 1720 | .then(() => redis.hincrby("existing_hash", "foo", -1)) 1721 | .then(res => { if (res !== 1) { throw 'unexpected value for hincrby result: ' + res } }) 1722 | .then(() => redis.hincrby("non_existing_hash", "foo", 1)) 1723 | .then(res => { if (res !== 1) { throw 'unexpected value for hincrby result: ' + res } }) 1724 | `, rs.Addr())) 1725 | 1726 | return err 1727 | }) 1728 | 1729 | assert.NoError(t, gotScriptErr) 1730 | assert.Equal(t, 3, rs.HandledCommandsCount()) 1731 | assert.Equal(t, [][]string{ 1732 | {"HELLO", "2"}, 1733 | {"HINCRBY", "existing_hash", "foo", "1"}, 1734 | {"HINCRBY", "existing_hash", "foo", "-1"}, 1735 | {"HINCRBY", "non_existing_hash", "foo", "1"}, 1736 | }, rs.GotCommands()) 1737 | } 1738 | 1739 | func TestClientSadd(t *testing.T) { 1740 | t.Parallel() 1741 | 1742 | ts := newTestSetup(t) 1743 | rs := RunT(t) 1744 | barWasSet := false 1745 | rs.RegisterCommandHandler("SADD", func(c *Connection, args []string) { 1746 | if len(args) != 2 { 1747 | c.WriteError(errors.New("ERR unexpected number of arguments for 'SADD' command")) 1748 | return 1749 | } 1750 | 1751 | if args[0] == "non_existing_set" { //nolint:goconst 1752 | c.WriteInteger(1) 1753 | return 1754 | } 1755 | 1756 | if barWasSet == false { 1757 | barWasSet = true 1758 | c.WriteInteger(1) 1759 | return 1760 | } 1761 | 1762 | c.WriteInteger(0) 1763 | }) 1764 | 1765 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1766 | _, err := ts.rt.RunString(fmt.Sprintf(` 1767 | const redis = new Client('redis://%s'); 1768 | 1769 | redis.sadd("existing_set", "bar") 1770 | .then(res => { if (res !== 1) { throw 'unexpected value for sadd result: ' + res } }) 1771 | .then(() => redis.sadd("existing_set", "bar")) 1772 | .then(res => { if (res !== 0) { throw 'unexpected value for sadd result: ' + res } }) 1773 | .then(() => redis.sadd("non_existing_set", "foo")) 1774 | .then(res => { if (res !== 1) { throw 'unexpected value for sadd result: ' + res} }) 1775 | `, rs.Addr())) 1776 | 1777 | return err 1778 | }) 1779 | 1780 | assert.NoError(t, gotScriptErr) 1781 | assert.Equal(t, 3, rs.HandledCommandsCount()) 1782 | assert.Equal(t, [][]string{ 1783 | {"HELLO", "2"}, 1784 | {"SADD", "existing_set", "bar"}, 1785 | {"SADD", "existing_set", "bar"}, 1786 | {"SADD", "non_existing_set", "foo"}, 1787 | }, rs.GotCommands()) 1788 | } 1789 | 1790 | func TestClientSrem(t *testing.T) { 1791 | t.Parallel() 1792 | 1793 | ts := newTestSetup(t) 1794 | rs := RunT(t) 1795 | fooWasRemoved := false 1796 | rs.RegisterCommandHandler("SREM", func(c *Connection, args []string) { 1797 | if len(args) != 2 { 1798 | c.WriteError(errors.New("ERR unexpected number of arguments for 'SREM' command")) 1799 | return 1800 | } 1801 | 1802 | if args[0] == "non_existing_set" { 1803 | c.WriteInteger(0) 1804 | return 1805 | } 1806 | 1807 | if fooWasRemoved == false { 1808 | fooWasRemoved = true 1809 | c.WriteInteger(1) 1810 | return 1811 | } 1812 | 1813 | c.WriteInteger(0) 1814 | }) 1815 | 1816 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1817 | _, err := ts.rt.RunString(fmt.Sprintf(` 1818 | const redis = new Client('redis://%s'); 1819 | 1820 | redis.srem("existing_set", "foo") 1821 | .then(res => { if (res !== 1) { throw 'unexpected value for srem result: ' + res } }) 1822 | .then(() => redis.srem("existing_set", "foo")) 1823 | .then(res => { if (res !== 0) { throw 'unexpected value for srem result: ' + res } }) 1824 | .then(() => redis.srem("existing_set", "doesnotexist")) 1825 | .then(res => { if (res !== 0) { throw 'unexpected value for srem result: ' + res } }) 1826 | .then(() => redis.srem("non_existing_set", "foo")) 1827 | .then(res => { if (res !== 0) { throw 'unexpected value for srem result: ' + res} }) 1828 | `, rs.Addr())) 1829 | 1830 | return err 1831 | }) 1832 | 1833 | assert.NoError(t, gotScriptErr) 1834 | assert.Equal(t, 4, rs.HandledCommandsCount()) 1835 | assert.Equal(t, [][]string{ 1836 | {"HELLO", "2"}, 1837 | {"SREM", "existing_set", "foo"}, 1838 | {"SREM", "existing_set", "foo"}, 1839 | {"SREM", "existing_set", "doesnotexist"}, 1840 | {"SREM", "non_existing_set", "foo"}, 1841 | }, rs.GotCommands()) 1842 | } 1843 | 1844 | func TestClientSismember(t *testing.T) { 1845 | t.Parallel() 1846 | 1847 | ts := newTestSetup(t) 1848 | rs := RunT(t) 1849 | rs.RegisterCommandHandler("SISMEMBER", func(c *Connection, args []string) { 1850 | if len(args) != 2 { 1851 | c.WriteError(errors.New("ERR unexpected number of arguments for 'SISMEMBER' command")) 1852 | return 1853 | } 1854 | 1855 | if args[0] == "non_existing_set" { 1856 | c.WriteInteger(0) 1857 | return 1858 | } 1859 | 1860 | if args[1] == "foo" { 1861 | c.WriteInteger(1) 1862 | return 1863 | } 1864 | 1865 | c.WriteInteger(0) 1866 | }) 1867 | 1868 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1869 | _, err := ts.rt.RunString(fmt.Sprintf(` 1870 | const redis = new Client('redis://%s'); 1871 | 1872 | redis.sismember("existing_set", "foo") 1873 | .then(res => { if (res !== true) { throw 'unexpected value for sismember result: ' + res } }) 1874 | .then(() => redis.sismember("existing_set", "bar")) 1875 | .then(res => { if (res !== false) { throw 'unexpected value for sismember result: ' + res } }) 1876 | .then(() => redis.sismember("non_existing_set", "foo")) 1877 | .then(res => { if (res !== false) { throw 'unexpected value for sismember result: ' + res} }) 1878 | `, rs.Addr())) 1879 | 1880 | return err 1881 | }) 1882 | 1883 | assert.NoError(t, gotScriptErr) 1884 | assert.Equal(t, 3, rs.HandledCommandsCount()) 1885 | assert.Equal(t, [][]string{ 1886 | {"HELLO", "2"}, 1887 | {"SISMEMBER", "existing_set", "foo"}, 1888 | {"SISMEMBER", "existing_set", "bar"}, 1889 | {"SISMEMBER", "non_existing_set", "foo"}, 1890 | }, rs.GotCommands()) 1891 | } 1892 | 1893 | func TestClientSmembers(t *testing.T) { 1894 | t.Parallel() 1895 | 1896 | ts := newTestSetup(t) 1897 | rs := RunT(t) 1898 | rs.RegisterCommandHandler("SMEMBERS", func(c *Connection, args []string) { 1899 | if len(args) != 1 { 1900 | c.WriteError(errors.New("ERR unexpected number of arguments for 'SMEMBERS' command")) 1901 | return 1902 | } 1903 | 1904 | if args[0] == "non_existing_set" { 1905 | c.WriteArray() 1906 | return 1907 | } 1908 | 1909 | c.WriteArray("foo", "bar") 1910 | }) 1911 | 1912 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1913 | _, err := ts.rt.RunString(fmt.Sprintf(` 1914 | const redis = new Client('redis://%s'); 1915 | 1916 | redis.smembers("existing_set") 1917 | .then(res => { if (res.length !== 2 || 'foo' in res || 'bar' in res) { throw 'unexpected value for smembers result: ' + res } }) 1918 | .then(() => redis.smembers("non_existing_set")) 1919 | .then(res => { if (res.length !== 0) { throw 'unexpected value for smembers result: ' + res} }) 1920 | `, rs.Addr())) 1921 | 1922 | return err 1923 | }) 1924 | 1925 | assert.NoError(t, gotScriptErr) 1926 | assert.Equal(t, 2, rs.HandledCommandsCount()) 1927 | assert.Equal(t, [][]string{ 1928 | {"HELLO", "2"}, 1929 | {"SMEMBERS", "existing_set"}, 1930 | {"SMEMBERS", "non_existing_set"}, 1931 | }, rs.GotCommands()) 1932 | } 1933 | 1934 | func TestClientSrandmember(t *testing.T) { 1935 | t.Parallel() 1936 | 1937 | ts := newTestSetup(t) 1938 | rs := RunT(t) 1939 | rs.RegisterCommandHandler("SRANDMEMBER", func(c *Connection, args []string) { 1940 | if len(args) != 1 { 1941 | c.WriteError(errors.New("ERR unexpected number of arguments for 'SRANDMEMBER' command")) 1942 | return 1943 | } 1944 | 1945 | if args[0] == "non_existing_set" { 1946 | c.WriteError(errors.New("ERR no elements in set")) 1947 | return 1948 | } 1949 | 1950 | c.WriteBulkString("foo") 1951 | }) 1952 | 1953 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1954 | _, err := ts.rt.RunString(fmt.Sprintf(` 1955 | const redis = new Client('redis://%s'); 1956 | 1957 | redis.srandmember("existing_set") 1958 | .then(res => { if (res !== 'foo' && res !== 'bar') { throw 'unexpected value for srandmember result: ' + res} }) 1959 | .then(() => redis.srandmember("non_existing_set")) 1960 | .then( 1961 | res => { throw 'unexpectedly resolved promise for srandmember result: ' + res }, 1962 | err => { if (err.error() !== 'ERR no elements in set') { throw 'unexpected error for srandmember operation: ' + err.error() } } 1963 | ) 1964 | `, rs.Addr())) 1965 | 1966 | return err 1967 | }) 1968 | 1969 | assert.NoError(t, gotScriptErr) 1970 | assert.Equal(t, 2, rs.HandledCommandsCount()) 1971 | assert.Equal(t, [][]string{ 1972 | {"HELLO", "2"}, 1973 | {"SRANDMEMBER", "existing_set"}, 1974 | {"SRANDMEMBER", "non_existing_set"}, 1975 | }, rs.GotCommands()) 1976 | } 1977 | 1978 | func TestClientSpop(t *testing.T) { 1979 | t.Parallel() 1980 | 1981 | ts := newTestSetup(t) 1982 | rs := RunT(t) 1983 | rs.RegisterCommandHandler("SPOP", func(c *Connection, args []string) { 1984 | if len(args) != 1 { 1985 | c.WriteError(errors.New("ERR unexpected number of arguments for 'SPOP' command")) 1986 | return 1987 | } 1988 | 1989 | if args[0] == "non_existing_set" { 1990 | c.WriteError(errors.New("ERR no elements in set")) 1991 | return 1992 | } 1993 | 1994 | c.WriteBulkString("foo") 1995 | }) 1996 | 1997 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 1998 | _, err := ts.rt.RunString(fmt.Sprintf(` 1999 | const redis = new Client('redis://%s'); 2000 | 2001 | redis.spop("existing_set") 2002 | .then(res => { if (res !== 'foo' && res !== 'bar') { throw 'unexpected value for spop result: ' + res} }) 2003 | .then(() => redis.spop("non_existing_set")) 2004 | .then( 2005 | res => { throw 'unexpectedly resolved promise for spop result: ' + res }, 2006 | err => { if (err.error() !== 'ERR no elements in set') { throw 'unexpected error for srandmember operation: ' + err.error() } } 2007 | ) 2008 | `, rs.Addr())) 2009 | 2010 | return err 2011 | }) 2012 | 2013 | assert.NoError(t, gotScriptErr) 2014 | assert.Equal(t, 2, rs.HandledCommandsCount()) 2015 | assert.Equal(t, [][]string{ 2016 | {"HELLO", "2"}, 2017 | {"SPOP", "existing_set"}, 2018 | {"SPOP", "non_existing_set"}, 2019 | }, rs.GotCommands()) 2020 | } 2021 | 2022 | func TestClientSendCommand(t *testing.T) { 2023 | t.Parallel() 2024 | 2025 | ts := newTestSetup(t) 2026 | rs := RunT(t) 2027 | fooWasSet := false 2028 | rs.RegisterCommandHandler("SADD", func(c *Connection, args []string) { 2029 | if len(args) != 2 { 2030 | c.WriteError(errors.New("ERR unexpected number of arguments for 'SADD' command")) 2031 | return 2032 | } 2033 | 2034 | if args[1] == "foo" && !fooWasSet { 2035 | fooWasSet = true 2036 | c.WriteInteger(1) 2037 | return 2038 | } 2039 | 2040 | c.WriteInteger(0) 2041 | }) 2042 | 2043 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 2044 | _, err := ts.rt.RunString(fmt.Sprintf(` 2045 | const redis = new Client('redis://%s'); 2046 | 2047 | redis.sendCommand("sadd", "existing_set", "foo") 2048 | .then(res => { if (res !== 1) { throw 'unexpected value for sadd result: ' + res } }) 2049 | .then(() => redis.sendCommand("sadd", "existing_set", "foo")) 2050 | .then(res => { if (res !== 0) { throw 'unexpected value for sadd result: ' + res } }) 2051 | 2052 | `, rs.Addr())) 2053 | 2054 | return err 2055 | }) 2056 | 2057 | assert.NoError(t, gotScriptErr) 2058 | assert.Equal(t, 2, rs.HandledCommandsCount()) 2059 | assert.Equal(t, [][]string{ 2060 | {"HELLO", "2"}, 2061 | {"SADD", "existing_set", "foo"}, 2062 | {"SADD", "existing_set", "foo"}, 2063 | }, rs.GotCommands()) 2064 | } 2065 | 2066 | func TestClientCommandsInInitContext(t *testing.T) { 2067 | t.Parallel() 2068 | 2069 | testCases := []struct { 2070 | name string 2071 | statement string 2072 | }{ 2073 | { 2074 | name: "set should fail when used in the init context", 2075 | statement: "redis.set('should', 'fail')", 2076 | }, 2077 | { 2078 | name: "get should fail when used in the init context", 2079 | statement: "redis.get('shouldfail')", 2080 | }, 2081 | { 2082 | name: "getSet should fail when used in the init context", 2083 | statement: "redis.getSet('should', 'fail')", 2084 | }, 2085 | { 2086 | name: "del should fail when used in the init context", 2087 | statement: "redis.del('should', 'fail')", 2088 | }, 2089 | { 2090 | name: "getDel should fail when used in the init context", 2091 | statement: "redis.getDel('shouldfail')", 2092 | }, 2093 | { 2094 | name: "exists should fail when used in the init context", 2095 | statement: "redis.exists('should', 'fail')", 2096 | }, 2097 | { 2098 | name: "incr should fail when used in the init context", 2099 | statement: "redis.incr('shouldfail')", 2100 | }, 2101 | { 2102 | name: "incrBy should fail when used in the init context", 2103 | statement: "redis.incrBy('shouldfail', 10)", 2104 | }, 2105 | { 2106 | name: "decr should fail when used in the init context", 2107 | statement: "redis.decr('shouldfail')", 2108 | }, 2109 | { 2110 | name: "decrBy should fail when used in the init context", 2111 | statement: "redis.decrBy('shouldfail', 10)", 2112 | }, 2113 | { 2114 | name: "randomKey should fail when used in the init context", 2115 | statement: "redis.randomKey()", 2116 | }, 2117 | { 2118 | name: "mget should fail when used in the init context", 2119 | statement: "redis.mget('should', 'fail')", 2120 | }, 2121 | { 2122 | name: "expire should fail when used in the init context", 2123 | statement: "redis.expire('shouldfail', 10)", 2124 | }, 2125 | { 2126 | name: "ttl should fail when used in the init context", 2127 | statement: "redis.ttl('shouldfail')", 2128 | }, 2129 | { 2130 | name: "persist should fail when used in the init context", 2131 | statement: "redis.persist('shouldfail')", 2132 | }, 2133 | { 2134 | name: "lpush should fail when used in the init context", 2135 | statement: "redis.lpush('should', 'fail', 'indeed')", 2136 | }, 2137 | { 2138 | name: "rpush should fail when used in the init context", 2139 | statement: "redis.rpush('should', 'fail', 'indeed')", 2140 | }, 2141 | { 2142 | name: "lpop should fail when used in the init context", 2143 | statement: "redis.lpop('shouldfail')", 2144 | }, 2145 | { 2146 | name: "rpop should fail when used in the init context", 2147 | statement: "redis.rpop('shouldfail')", 2148 | }, 2149 | { 2150 | name: "lrange should fail when used in the init context", 2151 | statement: "redis.lrange('shouldfail', 0, 5)", 2152 | }, 2153 | { 2154 | name: "lindex should fail when used in the init context", 2155 | statement: "redis.lindex('shouldfail', 1)", 2156 | }, 2157 | { 2158 | name: "lset should fail when used in the init context", 2159 | statement: "redis.lset('shouldfail', 1, 'fail')", 2160 | }, 2161 | { 2162 | name: "lrem should fail when used in the init context", 2163 | statement: "redis.lrem('should', 1, 'fail')", 2164 | }, 2165 | { 2166 | name: "llen should fail when used in the init context", 2167 | statement: "redis.llen('shouldfail')", 2168 | }, 2169 | { 2170 | name: "hset should fail when used in the init context", 2171 | statement: "redis.hset('shouldfail', 'foo', 'bar')", 2172 | }, 2173 | { 2174 | name: "hsetnx should fail when used in the init context", 2175 | statement: "redis.hsetnx('shouldfail', 'foo', 'bar')", 2176 | }, 2177 | { 2178 | name: "hget should fail when used in the init context", 2179 | statement: "redis.hget('should', 'fail')", 2180 | }, 2181 | { 2182 | name: "hdel should fail when used in the init context", 2183 | statement: "redis.hdel('should', 'fail', 'indeed')", 2184 | }, 2185 | { 2186 | name: "hgetall should fail when used in the init context", 2187 | statement: "redis.hgetall('shouldfail')", 2188 | }, 2189 | { 2190 | name: "hkeys should fail when used in the init context", 2191 | statement: "redis.hkeys('shouldfail')", 2192 | }, 2193 | { 2194 | name: "hvals should fail when used in the init context", 2195 | statement: "redis.hvals('shouldfail')", 2196 | }, 2197 | { 2198 | name: "hlen should fail when used in the init context", 2199 | statement: "redis.hlen('shouldfail')", 2200 | }, 2201 | { 2202 | name: "hincrby should fail when used in the init context", 2203 | statement: "redis.hincrby('should', 'fail', 10)", 2204 | }, 2205 | { 2206 | name: "sadd should fail when used in the init context", 2207 | statement: "redis.sadd('should', 'fail', 'indeed')", 2208 | }, 2209 | { 2210 | name: "srem should fail when used in the init context", 2211 | statement: "redis.srem('should', 'fail', 'indeed')", 2212 | }, 2213 | { 2214 | name: "sismember should fail when used in the init context", 2215 | statement: "redis.sismember('should', 'fail')", 2216 | }, 2217 | { 2218 | name: "smembers should fail when used in the init context", 2219 | statement: "redis.smembers('shouldfail')", 2220 | }, 2221 | { 2222 | name: "srandmember should fail when used in the init context", 2223 | statement: "redis.srandmember('shouldfail')", 2224 | }, 2225 | { 2226 | name: "persist should fail when used in the init context", 2227 | statement: "redis.persist('shouldfail')", 2228 | }, 2229 | { 2230 | name: "spop should fail when used in the init context", 2231 | statement: "redis.spop('shouldfail')", 2232 | }, 2233 | { 2234 | name: "sendCommand should fail when used in the init context", 2235 | statement: "redis.sendCommand('GET', 'shouldfail')", 2236 | }, 2237 | } 2238 | 2239 | for _, tc := range testCases { 2240 | t.Run(tc.name, func(t *testing.T) { 2241 | t.Parallel() 2242 | 2243 | ts := newInitContextTestSetup(t) 2244 | 2245 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 2246 | _, err := ts.rt.RunString(fmt.Sprintf(` 2247 | const redis = new Client('redis://unreachable:42424'); 2248 | 2249 | %s.then(res => { throw 'expected to fail when called in the init context' }) 2250 | `, tc.statement)) 2251 | 2252 | return err 2253 | }) 2254 | 2255 | assert.Error(t, gotScriptErr) 2256 | }) 2257 | } 2258 | } 2259 | 2260 | func TestClientCommandsAgainstUnreachableServer(t *testing.T) { 2261 | t.Parallel() 2262 | 2263 | testCases := []struct { 2264 | name string 2265 | statement string 2266 | }{ 2267 | { 2268 | name: "set should fail when server is unreachable", 2269 | statement: "redis.set('should', 'fail')", 2270 | }, 2271 | { 2272 | name: "get should fail when server is unreachable", 2273 | statement: "redis.get('shouldfail')", 2274 | }, 2275 | { 2276 | name: "getSet should fail when server is unreachable", 2277 | statement: "redis.getSet('should', 'fail')", 2278 | }, 2279 | { 2280 | name: "del should fail when server is unreachable", 2281 | statement: "redis.del('should', 'fail')", 2282 | }, 2283 | { 2284 | name: "getDel should fail when server is unreachable", 2285 | statement: "redis.getDel('shouldfail')", 2286 | }, 2287 | { 2288 | name: "exists should fail when server is unreachable", 2289 | statement: "redis.exists('should', 'fail')", 2290 | }, 2291 | { 2292 | name: "incr should fail when server is unreachable", 2293 | statement: "redis.incr('shouldfail')", 2294 | }, 2295 | { 2296 | name: "incrBy should fail when server is unreachable", 2297 | statement: "redis.incrBy('shouldfail', 10)", 2298 | }, 2299 | { 2300 | name: "decr should fail when server is unreachable", 2301 | statement: "redis.decr('shouldfail')", 2302 | }, 2303 | { 2304 | name: "decrBy should fail when server is unreachable", 2305 | statement: "redis.decrBy('shouldfail', 10)", 2306 | }, 2307 | { 2308 | name: "randomKey should fail when server is unreachable", 2309 | statement: "redis.randomKey()", 2310 | }, 2311 | { 2312 | name: "mget should fail when server is unreachable", 2313 | statement: "redis.mget('should', 'fail')", 2314 | }, 2315 | { 2316 | name: "expire should fail when server is unreachable", 2317 | statement: "redis.expire('shouldfail', 10)", 2318 | }, 2319 | { 2320 | name: "ttl should fail when server is unreachable", 2321 | statement: "redis.ttl('shouldfail')", 2322 | }, 2323 | { 2324 | name: "persist should fail when server is unreachable", 2325 | statement: "redis.persist('shouldfail')", 2326 | }, 2327 | { 2328 | name: "lpush should fail when server is unreachable", 2329 | statement: "redis.lpush('should', 'fail', 'indeed')", 2330 | }, 2331 | { 2332 | name: "rpush should fail when server is unreachable", 2333 | statement: "redis.rpush('should', 'fail', 'indeed')", 2334 | }, 2335 | { 2336 | name: "lpop should fail when server is unreachable", 2337 | statement: "redis.lpop('shouldfail')", 2338 | }, 2339 | { 2340 | name: "rpop should fail when server is unreachable", 2341 | statement: "redis.rpop('shouldfail')", 2342 | }, 2343 | { 2344 | name: "lrange should fail when server is unreachable", 2345 | statement: "redis.lrange('shouldfail', 0, 5)", 2346 | }, 2347 | { 2348 | name: "lindex should fail when server is unreachable", 2349 | statement: "redis.lindex('shouldfail', 1)", 2350 | }, 2351 | { 2352 | name: "lset should fail when server is unreachable", 2353 | statement: "redis.lset('shouldfail', 1, 'fail')", 2354 | }, 2355 | { 2356 | name: "lrem should fail when server is unreachable", 2357 | statement: "redis.lrem('should', 1, 'fail')", 2358 | }, 2359 | { 2360 | name: "llen should fail when server is unreachable", 2361 | statement: "redis.llen('shouldfail')", 2362 | }, 2363 | { 2364 | name: "hset should fail when server is unreachable", 2365 | statement: "redis.hset('shouldfail', 'foo', 'bar')", 2366 | }, 2367 | { 2368 | name: "hsetnx should fail when server is unreachable", 2369 | statement: "redis.hsetnx('shouldfail', 'foo', 'bar')", 2370 | }, 2371 | { 2372 | name: "hget should fail when server is unreachable", 2373 | statement: "redis.hget('should', 'fail')", 2374 | }, 2375 | { 2376 | name: "hdel should fail when server is unreachable", 2377 | statement: "redis.hdel('should', 'fail', 'indeed')", 2378 | }, 2379 | { 2380 | name: "hgetall should fail when server is unreachable", 2381 | statement: "redis.hgetall('shouldfail')", 2382 | }, 2383 | { 2384 | name: "hkeys should fail when server is unreachable", 2385 | statement: "redis.hkeys('shouldfail')", 2386 | }, 2387 | { 2388 | name: "hvals should fail when server is unreachable", 2389 | statement: "redis.hvals('shouldfail')", 2390 | }, 2391 | { 2392 | name: "hlen should fail when server is unreachable", 2393 | statement: "redis.hlen('shouldfail')", 2394 | }, 2395 | { 2396 | name: "hincrby should fail when server is unreachable", 2397 | statement: "redis.hincrby('should', 'fail', 10)", 2398 | }, 2399 | { 2400 | name: "sadd should fail when server is unreachable", 2401 | statement: "redis.sadd('should', 'fail', 'indeed')", 2402 | }, 2403 | { 2404 | name: "srem should fail when server is unreachable", 2405 | statement: "redis.srem('should', 'fail', 'indeed')", 2406 | }, 2407 | { 2408 | name: "sismember should fail when server is unreachable", 2409 | statement: "redis.sismember('should', 'fail')", 2410 | }, 2411 | { 2412 | name: "smembers should fail when server is unreachable", 2413 | statement: "redis.smembers('shouldfail')", 2414 | }, 2415 | { 2416 | name: "srandmember should fail when server is unreachable", 2417 | statement: "redis.srandmember('shouldfail')", 2418 | }, 2419 | { 2420 | name: "persist should fail when server is unreachable", 2421 | statement: "redis.persist('shouldfail')", 2422 | }, 2423 | { 2424 | name: "spop should fail when server is unreachable", 2425 | statement: "redis.spop('shouldfail')", 2426 | }, 2427 | { 2428 | name: "sendCommand should fail when server is unreachable", 2429 | statement: "redis.sendCommand('GET', 'shouldfail')", 2430 | }, 2431 | } 2432 | 2433 | for _, tc := range testCases { 2434 | t.Run(tc.name, func(t *testing.T) { 2435 | t.Parallel() 2436 | 2437 | ts := newTestSetup(t) 2438 | 2439 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 2440 | _, err := ts.rt.RunString(fmt.Sprintf(` 2441 | const redis = new Client('redis://unreachable:42424'); 2442 | 2443 | %s.then(res => { throw 'expected to fail when server is unreachable' }) 2444 | `, tc.statement)) 2445 | 2446 | return err 2447 | }) 2448 | 2449 | assert.Error(t, gotScriptErr) 2450 | }) 2451 | } 2452 | } 2453 | 2454 | func TestClientIsSupportedType(t *testing.T) { 2455 | t.Parallel() 2456 | 2457 | t.Run("table tests", func(t *testing.T) { 2458 | t.Parallel() 2459 | 2460 | tests := []struct { 2461 | name string 2462 | offset int 2463 | args []interface{} 2464 | wantErr bool 2465 | }{ 2466 | { 2467 | name: "string is a supported type", 2468 | offset: 1, 2469 | args: []interface{}{"foo"}, 2470 | wantErr: false, 2471 | }, 2472 | { 2473 | name: "int is a supported type", 2474 | offset: 1, 2475 | args: []interface{}{int(123)}, 2476 | wantErr: false, 2477 | }, 2478 | { 2479 | name: "int64 is a supported type", 2480 | offset: 1, 2481 | args: []interface{}{int64(123)}, 2482 | wantErr: false, 2483 | }, 2484 | { 2485 | name: "float64 is a supported type", 2486 | offset: 1, 2487 | args: []interface{}{float64(123)}, 2488 | wantErr: false, 2489 | }, 2490 | { 2491 | name: "bool is a supported type", 2492 | offset: 1, 2493 | args: []interface{}{bool(true)}, 2494 | wantErr: false, 2495 | }, 2496 | { 2497 | name: "multiple identical types args are supported", 2498 | offset: 1, 2499 | args: []interface{}{int(123), int(456)}, 2500 | wantErr: false, 2501 | }, 2502 | { 2503 | name: "multiple mixed types args are supported", 2504 | offset: 1, 2505 | args: []interface{}{int(123), "foo", bool(true)}, 2506 | wantErr: false, 2507 | }, 2508 | { 2509 | name: "slice[T] is not a supported type", 2510 | offset: 1, 2511 | args: []interface{}{[]string{"1", "2", "3"}}, 2512 | wantErr: true, 2513 | }, 2514 | { 2515 | name: "multiple mixed valid and invalid types args are not supported", 2516 | offset: 1, 2517 | args: []interface{}{int(123), []string{"1", "2", "3"}}, 2518 | wantErr: true, 2519 | }, 2520 | } 2521 | for _, tt := range tests { 2522 | t.Run(tt.name, func(t *testing.T) { 2523 | t.Parallel() 2524 | 2525 | c := &Client{} 2526 | gotErr := c.isSupportedType(tt.offset, tt.args...) 2527 | assert.Equal(t, 2528 | tt.wantErr, 2529 | gotErr != nil, 2530 | "Client.isSupportedType() error = %v, wantErr %v", gotErr, tt.wantErr, 2531 | ) 2532 | }) 2533 | } 2534 | }) 2535 | 2536 | t.Run("offset is respected in the error message", func(t *testing.T) { 2537 | t.Parallel() 2538 | 2539 | c := &Client{} 2540 | 2541 | gotErr := c.isSupportedType(3, int(123), []string{"1", "2", "3"}) 2542 | 2543 | assert.Error(t, gotErr) 2544 | assert.Contains(t, gotErr.Error(), "argument at index 4") 2545 | }) 2546 | } 2547 | 2548 | // testSetup is a helper struct holding components 2549 | // necessary to test the redis client, in the context 2550 | // of the execution of a k6 script. 2551 | type testSetup struct { 2552 | runtime *modulestest.Runtime 2553 | rt *sobek.Runtime 2554 | state *lib.State 2555 | samples chan metrics.SampleContainer 2556 | } 2557 | 2558 | // newTestSetup initializes a new test setup. 2559 | // It prepares a test setup with a mocked redis server and a goja runtime, 2560 | // and event loop, ready to execute scripts as if being executed in the 2561 | // main context of k6. 2562 | func newTestSetup(t testing.TB) testSetup { 2563 | ts := newInitContextTestSetup(t) 2564 | 2565 | state := &lib.State{ 2566 | Dialer: netext.NewDialer( 2567 | net.Dialer{}, 2568 | netext.NewResolver(net.LookupIP, 0, types.DNSfirst, types.DNSpreferIPv4), 2569 | ), 2570 | Options: lib.Options{ 2571 | SystemTags: metrics.NewSystemTagSet( 2572 | metrics.TagURL, 2573 | metrics.TagProto, 2574 | metrics.TagStatus, 2575 | metrics.TagSubproto, 2576 | ), 2577 | UserAgent: null.StringFrom("TestUserAgent"), 2578 | }, 2579 | Samples: ts.samples, 2580 | TLSConfig: &tls.Config{}, //nolint:gosec 2581 | BuiltinMetrics: metrics.RegisterBuiltinMetrics(metrics.NewRegistry()), 2582 | Tags: lib.NewVUStateTags(metrics.NewRegistry().RootTagSet()), 2583 | } 2584 | ts.runtime.MoveToVUContext(state) 2585 | 2586 | ts.state = state 2587 | return ts 2588 | } 2589 | 2590 | // newInitContextTestSetup initializes a new test setup. 2591 | // It prepares a test setup with a mocked redis server and a goja runtime, 2592 | // and event loop, ready to execute scripts as if being executed in the 2593 | // main context of k6. 2594 | func newInitContextTestSetup(t testing.TB) testSetup { 2595 | runtime := modulestest.NewRuntime(t) 2596 | samples := make(chan metrics.SampleContainer, 1000) 2597 | 2598 | rt := runtime.VU.RuntimeField 2599 | m := new(RootModule).NewModuleInstance(runtime.VU) 2600 | require.NoError(t, rt.Set("Client", m.Exports().Named["Client"])) 2601 | 2602 | return testSetup{ 2603 | runtime: runtime, 2604 | rt: rt, 2605 | samples: samples, 2606 | } 2607 | } 2608 | 2609 | func TestClientTLS(t *testing.T) { 2610 | t.Parallel() 2611 | 2612 | ts := newTestSetup(t) 2613 | ts.state.TLSConfig.InsecureSkipVerify = true 2614 | rs := RunTSecure(t, nil) 2615 | 2616 | err := ts.rt.Set("caCert", string(rs.TLSCertificate())) 2617 | require.NoError(t, err) 2618 | 2619 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 2620 | _, err := ts.rt.RunString(fmt.Sprintf(` 2621 | const redis = new Client({ 2622 | socket: { 2623 | host: '%s', 2624 | port: %d, 2625 | tls: { 2626 | ca: [caCert], 2627 | } 2628 | } 2629 | }); 2630 | 2631 | redis.sendCommand("PING"); 2632 | `, rs.Addr().IP.String(), rs.Addr().Port)) 2633 | 2634 | return err 2635 | }) 2636 | 2637 | require.NoError(t, gotScriptErr) 2638 | assert.Equal(t, 1, rs.HandledCommandsCount()) 2639 | assert.Equal(t, [][]string{ 2640 | {"HELLO", "2"}, 2641 | {"PING"}, 2642 | }, rs.GotCommands()) 2643 | } 2644 | 2645 | func TestClientTLSAuth(t *testing.T) { 2646 | t.Parallel() 2647 | 2648 | clientCert, clientPKey, err := generateTLSCert() 2649 | require.NoError(t, err) 2650 | 2651 | ts := newTestSetup(t) 2652 | ts.state.TLSConfig.InsecureSkipVerify = true 2653 | rs := RunTSecure(t, clientCert) 2654 | 2655 | err = ts.rt.Set("caCert", string(rs.TLSCertificate())) 2656 | require.NoError(t, err) 2657 | err = ts.rt.Set("clientCert", string(clientCert)) 2658 | require.NoError(t, err) 2659 | err = ts.rt.Set("clientPKey", string(clientPKey)) 2660 | require.NoError(t, err) 2661 | 2662 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 2663 | _, err := ts.rt.RunString(fmt.Sprintf(` 2664 | const redis = new Client({ 2665 | socket: { 2666 | host: '%s', 2667 | port: %d, 2668 | tls: { 2669 | ca: [caCert], 2670 | cert: clientCert, 2671 | key: clientPKey 2672 | } 2673 | } 2674 | }); 2675 | 2676 | redis.sendCommand("PING"); 2677 | `, rs.Addr().IP.String(), rs.Addr().Port)) 2678 | 2679 | return err 2680 | }) 2681 | 2682 | require.NoError(t, gotScriptErr) 2683 | assert.Equal(t, 1, rs.HandledCommandsCount()) 2684 | assert.Equal(t, [][]string{ 2685 | {"HELLO", "2"}, 2686 | {"PING"}, 2687 | }, rs.GotCommands()) 2688 | } 2689 | 2690 | func TestClientTLSRespectsNetworkOPtions(t *testing.T) { 2691 | t.Parallel() 2692 | 2693 | clientCert, clientPKey, err := generateTLSCert() 2694 | require.NoError(t, err) 2695 | 2696 | ts := newTestSetup(t) 2697 | rs := RunTSecure(t, clientCert) 2698 | 2699 | err = ts.rt.Set("caCert", string(rs.TLSCertificate())) 2700 | require.NoError(t, err) 2701 | err = ts.rt.Set("clientCert", string(clientCert)) 2702 | require.NoError(t, err) 2703 | err = ts.rt.Set("clientPKey", string(clientPKey)) 2704 | require.NoError(t, err) 2705 | 2706 | // Set the redis server's IP to be blacklisted. 2707 | net, err := lib.ParseCIDR(rs.Addr().IP.String() + "/32") 2708 | require.NoError(t, err) 2709 | ts.state.Dialer.(*netext.Dialer).Blacklist = []*lib.IPNet{net} 2710 | 2711 | gotScriptErr := ts.runtime.EventLoop.Start(func() error { 2712 | _, err := ts.rt.RunString(fmt.Sprintf(` 2713 | const redis = new Client({ 2714 | socket: { 2715 | host: '%s', 2716 | port: %d, 2717 | tls: { 2718 | ca: [caCert], 2719 | cert: clientCert, 2720 | key: clientPKey 2721 | } 2722 | } 2723 | }); 2724 | 2725 | // This operation triggers a connection to the redis 2726 | // server under the hood, and should therefore fail, since 2727 | // the server's IP is blacklisted by k6. 2728 | redis.sendCommand("PING") 2729 | `, rs.Addr().IP.String(), rs.Addr().Port)) 2730 | 2731 | return err 2732 | }) 2733 | 2734 | assert.Error(t, gotScriptErr) 2735 | assert.ErrorContains(t, gotScriptErr, "IP ("+rs.Addr().IP.String()+") is in a blacklisted range") 2736 | assert.Equal(t, 0, rs.HandledCommandsCount()) 2737 | } 2738 | --------------------------------------------------------------------------------