diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 74d3e003..ea114611 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -15,7 +15,11 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + + - name: Checkout tree + uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 diff --git a/.gitignore b/.gitignore index 6bb2ab16..90242944 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ .idea +.DS_Store bin volumes/nodes dist/ pkg/modules/*/aof pkg/echovault/aof dump.rdb -**/*/testdata \ No newline at end of file +**/*/testdata diff --git a/Makefile b/Makefile index d5b3c079..ef3f48ea 100644 --- a/Makefile +++ b/Makefile @@ -20,13 +20,11 @@ run: test: env RACE=false OUT=internal/modules/admin/testdata make build-modules-test && \ env RACE=false OUT=echovault/testdata make build-modules-test && \ - go clean -testcache && \ CGO_ENABLED=1 go test ./... -coverprofile coverage/coverage.out test-race: env RACE=true OUT=internal/modules/admin/testdata make build-modules-test && \ env RACE=true OUT=echovault/testdata make build-modules-test && \ - go clean -testcache && \ CGO_ENABLED=1 go test ./... --race cover: diff --git a/coverage/coverage.out b/coverage/coverage.out index 29214eb6..bdc92204 100644 --- a/coverage/coverage.out +++ b/coverage/coverage.out @@ -1,4 +1,107 @@ mode: set +github.com/echovault/echovault/internal/aof/engine.go:50.56,51.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:51.30,53.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:56.57,57.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:57.30,59.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:62.59,63.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:63.30,65.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:68.58,69.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:69.30,71.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:74.59,75.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:75.30,77.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:80.82,81.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:81.30,83.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:86.89,87.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:87.30,89.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:92.73,93.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:93.30,95.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:98.82,99.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:99.30,101.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:104.78,105.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:105.30,107.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:110.69,118.29 1 1 +github.com/echovault/echovault/internal/aof/engine.go:118.30,118.31 0 0 +github.com/echovault/echovault/internal/aof/engine.go:119.30,119.31 0 0 +github.com/echovault/echovault/internal/aof/engine.go:120.57,120.71 1 0 +github.com/echovault/echovault/internal/aof/engine.go:121.63,121.64 0 0 +github.com/echovault/echovault/internal/aof/engine.go:122.44,122.45 0 0 +github.com/echovault/echovault/internal/aof/engine.go:127.2,127.33 1 1 +github.com/echovault/echovault/internal/aof/engine.go:127.33,129.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:132.2,139.16 2 1 +github.com/echovault/echovault/internal/aof/engine.go:139.16,141.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:142.2,152.16 3 1 +github.com/echovault/echovault/internal/aof/engine.go:152.16,154.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:155.2,159.12 2 1 +github.com/echovault/echovault/internal/aof/engine.go:159.12,160.7 1 1 +github.com/echovault/echovault/internal/aof/engine.go:160.7,162.54 2 1 +github.com/echovault/echovault/internal/aof/engine.go:162.54,164.5 1 0 +github.com/echovault/echovault/internal/aof/engine.go:168.2,168.20 1 1 +github.com/echovault/echovault/internal/aof/engine.go:171.52,173.2 1 1 +github.com/echovault/echovault/internal/aof/engine.go:175.42,183.62 5 1 +github.com/echovault/echovault/internal/aof/engine.go:183.62,185.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:188.2,188.54 1 1 +github.com/echovault/echovault/internal/aof/engine.go:188.54,190.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:192.2,192.12 1 1 +github.com/echovault/echovault/internal/aof/engine.go:195.39,196.55 1 1 +github.com/echovault/echovault/internal/aof/engine.go:196.55,198.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:199.2,199.53 1 1 +github.com/echovault/echovault/internal/aof/engine.go:199.53,201.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:202.2,202.12 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:45.62,46.36 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:46.36,48.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:51.71,52.36 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:52.36,54.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:57.88,58.36 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:58.36,60.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:63.95,64.36 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:64.36,66.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:69.65,70.36 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:70.36,72.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:75.86,81.52 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:81.52,84.4 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:85.60,85.61 0 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:88.2,88.33 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:88.33,90.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:93.2,93.46 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:93.46,95.17 2 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:95.17,97.4 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:98.3,99.17 2 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:99.17,101.4 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:102.3,102.15 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:105.2,105.19 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:108.52,115.16 5 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:115.16,117.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:120.2,120.44 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:120.44,122.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:124.2,124.46 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:124.46,126.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:128.2,128.44 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:128.44,130.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:133.2,133.39 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:133.39,135.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:137.2,137.12 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:140.45,141.21 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:141.21,143.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:146.2,146.47 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:146.47,148.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:150.2,151.16 2 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:151.16,153.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:155.2,155.17 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:155.17,157.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:159.2,161.49 2 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:161.49,163.3 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:165.2,165.56 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:165.56,167.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:169.2,169.12 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:172.43,176.2 3 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:179.110,181.26 2 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:181.26,182.36 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:182.36,183.12 1 0 +github.com/echovault/echovault/internal/aof/preamble/store.go:185.3,185.43 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:185.43,187.4 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:189.2,189.35 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:189.35,191.3 1 1 +github.com/echovault/echovault/internal/aof/preamble/store.go:192.2,192.14 1 1 github.com/echovault/echovault/internal/aof/log/store.go:46.60,47.34 1 1 github.com/echovault/echovault/internal/aof/log/store.go:47.34,49.3 1 1 github.com/echovault/echovault/internal/aof/log/store.go:52.61,53.34 1 1 @@ -95,61 +198,6 @@ github.com/echovault/echovault/internal/config/config.go:247.2,249.45 2 0 github.com/echovault/echovault/internal/config/config.go:249.45,251.3 1 0 github.com/echovault/echovault/internal/config/config.go:253.2,253.18 1 0 github.com/echovault/echovault/internal/config/default.go:8.29,38.2 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:45.62,46.36 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:46.36,48.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:51.71,52.36 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:52.36,54.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:57.88,58.36 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:58.36,60.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:63.95,64.36 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:64.36,66.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:69.65,70.36 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:70.36,72.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:75.86,81.52 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:81.52,84.4 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:85.60,85.61 0 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:88.2,88.33 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:88.33,90.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:93.2,93.46 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:93.46,95.17 2 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:95.17,97.4 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:98.3,99.17 2 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:99.17,101.4 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:102.3,102.15 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:105.2,105.19 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:108.52,115.16 5 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:115.16,117.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:120.2,120.44 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:120.44,122.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:124.2,124.46 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:124.46,126.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:128.2,128.44 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:128.44,130.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:133.2,133.39 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:133.39,135.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:137.2,137.12 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:140.45,141.21 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:141.21,143.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:146.2,146.47 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:146.47,148.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:150.2,151.16 2 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:151.16,153.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:155.2,155.17 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:155.17,157.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:159.2,161.49 2 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:161.49,163.3 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:165.2,165.56 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:165.56,167.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:169.2,169.12 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:172.43,176.2 3 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:179.110,181.26 2 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:181.26,182.36 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:182.36,183.12 1 0 -github.com/echovault/echovault/internal/aof/preamble/store.go:185.3,185.43 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:185.43,187.4 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:189.2,189.35 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:189.35,191.3 1 1 -github.com/echovault/echovault/internal/aof/preamble/store.go:192.2,192.14 1 1 github.com/echovault/echovault/internal/eviction/lfu.go:35.29,42.2 3 1 github.com/echovault/echovault/internal/eviction/lfu.go:44.34,46.2 1 1 github.com/echovault/echovault/internal/eviction/lfu.go:48.44,50.54 1 1 @@ -184,497 +232,12 @@ github.com/echovault/echovault/internal/eviction/lru.go:92.73,94.3 1 0 github.com/echovault/echovault/internal/eviction/lru.go:95.2,95.19 1 0 github.com/echovault/echovault/internal/eviction/lru.go:95.19,97.3 1 0 github.com/echovault/echovault/internal/eviction/lru.go:100.50,103.2 2 1 -github.com/echovault/echovault/internal/aof/engine.go:50.56,51.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:51.30,53.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:56.57,57.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:57.30,59.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:62.59,63.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:63.30,65.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:68.58,69.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:69.30,71.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:74.59,75.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:75.30,77.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:80.82,81.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:81.30,83.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:86.89,87.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:87.30,89.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:92.73,93.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:93.30,95.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:98.82,99.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:99.30,101.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:104.78,105.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:105.30,107.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:110.69,118.29 1 1 -github.com/echovault/echovault/internal/aof/engine.go:118.30,118.31 0 0 -github.com/echovault/echovault/internal/aof/engine.go:119.30,119.31 0 0 -github.com/echovault/echovault/internal/aof/engine.go:120.57,120.71 1 0 -github.com/echovault/echovault/internal/aof/engine.go:121.63,121.64 0 0 -github.com/echovault/echovault/internal/aof/engine.go:122.44,122.45 0 0 -github.com/echovault/echovault/internal/aof/engine.go:127.2,127.33 1 1 -github.com/echovault/echovault/internal/aof/engine.go:127.33,129.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:132.2,139.16 2 1 -github.com/echovault/echovault/internal/aof/engine.go:139.16,141.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:142.2,152.16 3 1 -github.com/echovault/echovault/internal/aof/engine.go:152.16,154.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:155.2,159.12 2 1 -github.com/echovault/echovault/internal/aof/engine.go:159.12,160.7 1 1 -github.com/echovault/echovault/internal/aof/engine.go:160.7,162.54 2 1 -github.com/echovault/echovault/internal/aof/engine.go:162.54,164.5 1 0 -github.com/echovault/echovault/internal/aof/engine.go:168.2,168.20 1 1 -github.com/echovault/echovault/internal/aof/engine.go:171.52,173.2 1 1 -github.com/echovault/echovault/internal/aof/engine.go:175.42,183.62 5 1 -github.com/echovault/echovault/internal/aof/engine.go:183.62,185.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:188.2,188.54 1 1 -github.com/echovault/echovault/internal/aof/engine.go:188.54,190.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:192.2,192.12 1 1 -github.com/echovault/echovault/internal/aof/engine.go:195.39,196.55 1 1 -github.com/echovault/echovault/internal/aof/engine.go:196.55,198.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:199.2,199.53 1 1 -github.com/echovault/echovault/internal/aof/engine.go:199.53,201.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:202.2,202.12 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:52.40,57.24 3 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:57.24,65.3 2 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:68.2,68.28 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:68.28,70.54 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:70.54,71.14 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:72.9,73.17 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:73.17,74.37 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:74.37,76.6 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:79.4,81.22 2 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:81.22,82.61 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:82.61,84.6 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:87.4,87.39 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:87.39,88.61 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:88.61,90.6 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:96.2,97.29 2 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:97.29,98.33 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:98.33,100.9 2 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:103.2,103.20 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:103.20,105.3 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:108.2,108.29 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:108.29,110.3 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:112.2,122.13 3 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:125.52,130.70 3 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:130.70,132.3 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:133.2,137.3 2 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:140.45,146.33 3 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:146.33,147.30 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:147.30,148.47 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:148.47,150.5 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:150.10,153.5 2 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:157.2,158.45 2 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:158.45,160.3 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:162.2,169.12 4 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:172.41,177.2 3 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:179.73,184.37 4 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:184.37,185.28 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:185.28,187.12 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:190.3,190.31 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:190.31,191.30 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:191.30,193.5 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:196.3,196.18 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:196.18,197.12 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:200.3,200.52 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:200.52,201.49 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:201.49,203.5 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:206.3,206.63 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:206.63,208.4 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:210.2,210.12 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:213.95,222.19 6 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:222.19,230.60 3 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:230.60,232.4 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:233.3,233.24 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:236.2,236.19 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:236.19,245.31 4 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:245.31,246.28 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:246.28,249.10 3 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:252.3,252.17 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:252.17,254.4 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:258.2,258.19 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:258.19,260.3 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:263.2,263.21 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:263.21,269.3 2 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:271.2,271.46 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:271.46,272.38 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:272.38,275.18 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:275.18,282.5 2 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:286.2,286.50 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:289.131,298.16 6 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:298.16,300.3 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:302.2,306.59 4 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:306.59,310.17 4 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:310.17,312.4 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:316.2,316.36 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:316.36,318.3 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:321.2,321.43 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:321.43,323.3 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:326.2,326.37 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:326.37,328.3 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:331.2,334.29 2 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:334.29,336.3 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:339.2,339.57 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:339.57,341.3 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:344.2,345.65 2 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:345.65,346.101 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:346.101,347.63 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:347.63,349.5 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:350.4,351.16 2 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:353.5,354.27 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:354.27,356.4 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:357.3,357.88 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:361.2,361.64 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:361.64,362.101 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:362.101,363.63 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:363.63,366.5 2 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:367.4,367.16 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:369.5,371.3 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:374.2,374.94 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:374.94,376.3 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:376.5,378.3 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:381.2,381.93 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:381.93,383.3 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:383.5,385.3 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:388.2,388.59 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:388.59,390.36 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:390.36,392.106 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:392.106,394.5 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:394.7,396.5 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:398.4,398.105 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:398.105,400.5 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:400.7,402.5 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:404.3,404.13 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:407.2,407.45 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:407.45,409.29 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:409.29,411.4 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:414.3,414.59 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:414.59,415.95 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:415.95,416.49 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:416.49,418.6 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:419.5,420.17 2 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:422.6,424.4 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:427.3,427.60 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:427.60,428.97 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:428.97,429.50 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:429.50,431.6 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:432.5,433.17 2 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:435.6,437.4 1 0 -github.com/echovault/echovault/internal/modules/acl/acl.go:440.2,440.12 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:443.32,447.33 3 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:447.33,452.31 5 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:452.31,453.37 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:453.37,455.5 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:457.3,457.25 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:460.2,460.29 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:460.29,461.33 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:461.33,463.4 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:467.29,469.2 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:471.31,473.2 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:475.30,477.2 1 1 -github.com/echovault/echovault/internal/modules/acl/acl.go:479.32,481.2 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:31.68,32.56 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:32.56,34.3 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:35.2,36.9 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:36.9,38.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:39.2,39.102 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:39.102,41.3 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:42.2,42.42 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:45.71,46.30 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:46.30,48.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:50.2,51.9 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:51.9,53.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:55.2,57.30 3 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:57.30,58.38 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:58.38,61.9 3 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:65.2,65.16 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:65.16,67.3 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:70.2,74.18 3 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:74.18,76.3 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:76.8,78.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:79.2,79.21 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:79.21,81.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:82.2,82.17 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:82.17,84.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:86.2,87.29 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:87.29,89.3 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:92.2,93.51 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:93.51,94.22 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:94.22,96.12 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:98.3,98.49 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:100.2,100.51 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:100.51,101.22 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:101.22,103.12 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:105.3,105.49 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:109.2,110.48 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:110.48,111.21 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:111.21,113.12 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:115.3,115.47 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:117.2,117.48 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:117.48,118.21 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:118.21,120.12 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:122.3,122.47 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:126.2,127.79 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:127.79,128.37 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:128.37,130.4 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:132.2,133.30 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:133.30,134.10 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:135.100,137.53 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:138.53,140.52 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:141.52,143.52 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:148.2,150.54 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:150.54,152.3 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:153.2,153.54 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:153.54,155.3 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:157.2,159.25 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:162.67,163.29 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:163.29,165.3 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:167.2,171.35 3 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:171.35,172.36 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:172.36,173.48 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:173.48,175.5 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:176.4,176.12 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:178.3,178.50 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:178.50,179.51 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:179.51,182.5 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:186.2,186.30 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:186.30,189.34 3 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:189.34,192.4 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:193.3,194.28 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:194.28,196.24 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:196.24,198.5 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:200.3,200.26 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:203.2,203.30 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:203.30,205.46 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:205.46,206.54 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:206.54,208.38 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:208.38,210.30 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:210.30,212.7 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:214.5,214.28 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:219.2,219.85 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:222.69,224.9 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:224.9,226.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:227.2,228.33 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:228.33,230.3 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:231.2,232.25 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:235.71,237.9 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:237.9,239.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:240.2,240.56 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:240.56,242.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:243.2,243.42 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:246.71,247.29 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:247.29,249.3 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:250.2,251.9 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:251.9,253.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:254.2,254.75 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:254.75,256.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:257.2,257.42 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:260.70,262.9 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:262.9,264.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:265.2,266.74 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:269.68,270.29 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:270.29,272.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:273.2,274.9 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:274.9,276.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:277.2,279.33 3 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:279.33,282.19 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:282.19,284.4 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:284.9,286.4 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:288.3,288.22 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:288.22,290.4 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:292.3,292.18 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:292.18,294.4 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:296.3,296.43 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:296.43,297.61 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:297.61,299.5 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:300.4,300.58 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:300.58,302.5 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:305.3,305.52 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:305.52,306.23 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:306.23,308.13 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:310.4,310.39 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:313.3,313.52 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:313.52,314.23 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:314.23,316.13 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:318.4,318.39 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:321.3,321.49 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:321.49,322.22 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:322.22,324.13 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:326.4,326.37 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:329.3,329.49 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:329.49,330.22 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:330.22,332.13 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:334.4,334.37 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:337.3,337.45 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:337.45,338.52 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:338.52,340.13 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:342.4,342.41 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:345.3,345.45 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:345.45,346.52 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:346.52,348.5 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:351.3,351.55 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:351.55,353.4 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:355.3,355.55 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:355.55,357.4 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:358.3,358.54 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:361.2,362.25 2 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:365.68,366.30 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:366.30,368.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:370.2,371.9 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:371.9,373.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:375.2,379.16 4 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:379.16,381.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:383.2,383.15 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:383.15,384.35 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:384.35,386.4 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:389.2,393.20 3 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:393.20,394.59 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:394.59,396.4 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:399.2,399.37 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:399.37,400.59 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:400.59,402.4 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:406.2,406.29 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:406.29,410.31 3 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:410.31,411.35 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:411.35,414.54 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:414.54,416.6 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:416.11,419.6 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:420.5,420.10 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:424.3,424.17 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:424.17,426.4 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:429.2,429.42 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:432.68,433.29 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:433.29,435.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:437.2,438.9 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:438.9,440.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:442.2,446.16 4 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:446.16,448.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:450.2,450.15 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:450.15,451.35 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:451.35,453.4 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:456.2,458.20 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:458.20,461.17 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:461.17,463.4 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:464.3,465.17 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:465.17,467.4 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:470.2,470.37 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:470.37,473.17 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:473.17,475.4 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:476.3,477.17 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:477.17,479.4 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:482.2,483.16 2 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:483.16,485.3 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:487.2,487.42 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:490.36,500.84 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:500.84,506.5 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:515.84,521.5 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:530.86,536.7 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:545.86,551.7 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:560.86,566.7 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:575.86,581.7 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:591.86,597.7 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:606.86,612.7 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:621.86,627.7 1 1 -github.com/echovault/echovault/internal/modules/acl/commands.go:639.86,645.7 1 0 -github.com/echovault/echovault/internal/modules/acl/commands.go:654.86,660.7 1 0 -github.com/echovault/echovault/internal/modules/acl/user.go:53.31,55.39 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:55.39,57.3 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:58.2,59.51 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:59.51,61.3 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:63.2,64.37 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:64.37,66.3 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:67.2,68.49 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:68.49,70.3 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:72.2,73.53 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:73.53,75.3 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:76.2,77.54 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:77.54,79.3 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:81.2,82.43 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:82.43,84.3 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:85.2,86.55 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:86.55,88.3 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:91.79,93.32 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:93.32,94.24 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:94.24,96.12 2 0 -github.com/echovault/echovault/internal/modules/acl/user.go:98.3,98.25 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:100.2,100.33 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:100.33,101.17 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:101.17,104.4 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:105.3,105.25 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:107.2,107.8 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:110.50,111.26 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:111.26,113.35 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:113.35,115.4 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:116.3,116.36 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:116.36,118.4 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:120.3,120.37 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:120.37,126.12 3 1 -github.com/echovault/echovault/internal/modules/acl/user.go:128.3,128.20 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:128.20,129.84 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:129.84,130.65 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:130.65,132.6 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:133.5,133.45 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:135.4,135.12 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:137.3,137.20 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:137.20,138.84 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:138.84,139.68 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:139.68,141.6 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:142.5,142.45 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:144.4,144.12 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:147.3,147.43 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:147.43,150.12 3 1 -github.com/echovault/echovault/internal/modules/acl/user.go:152.3,152.46 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:152.46,154.12 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:156.3,156.36 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:156.36,157.21 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:157.21,159.13 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:161.4,161.21 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:161.21,163.13 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:167.3,167.40 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:167.40,171.12 4 0 -github.com/echovault/echovault/internal/modules/acl/user.go:173.3,173.93 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:173.93,178.12 5 1 -github.com/echovault/echovault/internal/modules/acl/user.go:180.3,180.57 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:180.57,183.12 3 1 -github.com/echovault/echovault/internal/modules/acl/user.go:185.3,185.57 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:185.57,188.12 3 1 -github.com/echovault/echovault/internal/modules/acl/user.go:191.3,191.44 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:191.44,193.12 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:195.3,195.36 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:195.36,196.21 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:196.21,198.13 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:200.4,200.21 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:200.21,202.13 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:206.3,206.44 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:206.44,209.12 3 1 -github.com/echovault/echovault/internal/modules/acl/user.go:211.3,211.66 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:211.66,212.21 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:212.21,214.13 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:216.4,216.21 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:216.21,218.13 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:224.2,224.26 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:224.26,225.39 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:225.39,228.4 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:231.2,231.26 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:231.26,233.42 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:233.42,236.4 2 0 -github.com/echovault/echovault/internal/modules/acl/user.go:238.3,238.43 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:238.43,243.4 4 1 -github.com/echovault/echovault/internal/modules/acl/user.go:245.3,245.42 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:245.42,249.4 3 1 -github.com/echovault/echovault/internal/modules/acl/user.go:251.3,251.46 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:251.46,254.4 2 1 -github.com/echovault/echovault/internal/modules/acl/user.go:256.2,256.12 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:259.36,273.2 13 0 -github.com/echovault/echovault/internal/modules/acl/user.go:275.38,288.2 12 0 -github.com/echovault/echovault/internal/modules/acl/user.go:290.40,305.2 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:307.46,308.24 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:308.24,310.3 1 1 -github.com/echovault/echovault/internal/modules/acl/user.go:311.2,311.26 1 1 github.com/echovault/echovault/internal/modules/connection/commands.go:24.68,25.29 1 1 github.com/echovault/echovault/internal/modules/connection/commands.go:26.10,27.54 1 1 github.com/echovault/echovault/internal/modules/connection/commands.go:28.9,29.34 1 1 github.com/echovault/echovault/internal/modules/connection/commands.go:30.9,31.94 1 1 github.com/echovault/echovault/internal/modules/connection/commands.go:35.36,45.84 1 1 -github.com/echovault/echovault/internal/modules/connection/commands.go:45.84,51.5 1 0 +github.com/echovault/echovault/internal/modules/connection/commands.go:45.84,51.5 1 1 github.com/echovault/echovault/internal/modules/generic/commands.go:33.67,35.16 2 1 github.com/echovault/echovault/internal/modules/generic/commands.go:35.16,37.3 1 0 github.com/echovault/echovault/internal/modules/generic/commands.go:39.2,46.16 7 1 @@ -997,7 +560,7 @@ github.com/echovault/echovault/internal/modules/hash/commands.go:301.38,303.17 2 github.com/echovault/echovault/internal/modules/hash/commands.go:303.17,304.41 1 1 github.com/echovault/echovault/internal/modules/hash/commands.go:304.41,306.13 2 1 github.com/echovault/echovault/internal/modules/hash/commands.go:308.4,308.42 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:308.42,311.13 3 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:308.42,311.13 3 0 github.com/echovault/echovault/internal/modules/hash/commands.go:313.4,313.38 1 1 github.com/echovault/echovault/internal/modules/hash/commands.go:313.38,315.13 2 1 github.com/echovault/echovault/internal/modules/hash/commands.go:320.2,320.25 1 1 @@ -1317,157 +880,108 @@ github.com/echovault/echovault/internal/modules/list/key_funcs.go:115.2,119.8 1 github.com/echovault/echovault/internal/modules/list/key_funcs.go:122.75,123.19 1 1 github.com/echovault/echovault/internal/modules/list/key_funcs.go:123.19,125.3 1 1 github.com/echovault/echovault/internal/modules/list/key_funcs.go:126.2,130.8 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:34.51,35.32 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:35.32,37.3 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:41.57,42.32 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:42.32,45.3 2 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:48.61,59.33 3 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:59.33,61.3 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:63.2,63.16 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:66.28,67.12 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:67.12,68.7 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:68.7,73.40 3 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:73.40,74.30 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:74.30,79.21 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:79.21,81.7 1 0 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:85.4,85.33 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:90.34,92.2 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:94.40,96.2 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:98.51,101.40 3 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:101.40,103.3 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:104.2,105.11 2 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:108.53,111.40 3 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:111.40,113.3 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:114.2,115.13 2 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:118.44,120.2 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:122.36,129.2 4 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:131.34,138.2 4 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:140.59,145.35 4 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:145.35,147.3 1 1 -github.com/echovault/echovault/internal/modules/pubsub/channel.go:149.2,149.20 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:25.73,27.9 2 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:27.9,29.3 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:31.2,33.24 2 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:33.24,35.3 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:37.2,40.17 3 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:43.75,45.9 2 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:45.9,47.3 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:49.2,53.90 3 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:56.71,58.9 2 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:58.9,60.3 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:61.2,61.30 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:61.30,63.3 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:64.2,65.42 2 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:68.78,69.29 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:69.29,71.3 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:73.2,74.9 2 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:74.9,76.3 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:78.2,79.30 2 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:79.30,81.3 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:83.2,83.38 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:86.76,88.9 2 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:88.9,90.3 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:91.2,92.49 2 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:95.77,97.9 2 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:97.9,99.3 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:100.2,100.47 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:103.36,111.84 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:111.84,113.21 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:113.21,115.6 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:116.5,120.11 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:130.84,132.21 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:132.21,134.6 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:135.5,139.11 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:149.84,151.22 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:151.22,153.6 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:154.5,158.11 1 1 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:170.84,177.5 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:188.84,194.5 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:203.84,209.5 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:210.68,212.5 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:222.86,228.7 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:237.86,243.7 1 0 -github.com/echovault/echovault/internal/modules/pubsub/commands.go:253.86,259.7 1 0 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:33.26,38.2 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:40.101,47.17 5 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:47.17,49.3 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:51.2,51.37 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:51.37,55.75 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:55.75,57.4 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:59.3,59.23 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:59.23,62.19 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:62.19,64.5 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:64.10,66.5 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:67.4,68.31 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:68.31,73.20 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:73.20,75.6 1 0 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:76.5,76.47 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:78.9,80.47 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:80.47,85.20 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:85.20,87.6 1 0 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:93.110,98.17 4 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:98.17,100.3 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:102.2,105.24 3 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:105.24,106.19 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:106.19,109.40 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:109.40,110.31 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:110.31,111.14 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:113.5,113.34 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:113.34,116.6 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:118.9,121.40 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:121.40,122.31 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:122.31,123.14 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:125.5,125.34 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:125.34,128.6 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:136.2,136.38 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:136.38,137.30 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:137.30,138.54 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:138.54,141.5 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:147.2,147.17 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:147.17,148.36 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:148.36,150.40 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:150.40,152.58 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:152.58,153.35 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:153.35,156.7 2 0 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:157.6,157.14 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:160.5,160.30 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:160.30,161.35 1 0 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:161.35,164.7 2 0 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:170.2,171.39 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:171.39,173.3 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:175.2,175.20 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:178.82,182.38 3 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:182.38,184.29 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:184.29,185.35 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:185.35,187.5 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:188.4,188.12 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:191.3,191.41 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:191.41,193.4 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:197.51,204.19 5 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:204.19,205.39 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:205.39,206.26 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:206.26,209.5 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:211.3,212.21 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:215.2,217.38 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:217.38,219.78 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:219.78,222.12 3 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:225.3,225.50 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:225.50,228.4 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:231.2,231.53 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:234.32,239.38 4 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:239.38,240.51 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:240.51,242.4 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:244.2,244.14 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:247.52,252.35 4 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:252.35,254.66 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:254.66,256.4 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:257.3,257.20 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:257.20,259.12 2 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:261.3,261.106 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:263.2,263.20 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:266.47,271.38 4 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:271.38,273.3 1 1 -github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:275.2,275.17 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:55.56,56.30 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:56.30,58.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:61.59,62.30 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:62.30,64.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:67.64,68.30 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:68.30,70.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:73.59,74.30 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:74.30,76.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:79.59,80.30 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:80.30,82.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:85.60,86.30 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:86.30,88.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:91.82,92.30 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:92.30,94.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:97.77,98.30 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:98.30,100.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:103.73,104.30 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:104.30,106.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:109.89,110.30 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:110.30,112.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:115.65,122.30 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:122.31,122.32 0 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:123.31,123.32 0 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:124.52,126.4 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:127.71,127.72 0 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:128.48,128.49 0 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:129.43,131.4 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:134.2,134.33 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:134.33,136.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:138.2,138.34 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:138.34,139.13 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:139.13,140.8 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:140.8,142.62 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:142.62,143.50 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:143.50,145.7 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:151.2,151.15 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:154.44,174.58 7 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:174.58,177.3 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:180.2,182.16 3 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:182.16,183.37 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:183.37,186.18 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:186.18,189.5 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:190.4,190.24 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:191.9,194.4 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:197.2,198.16 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:198.16,201.3 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:202.2,202.35 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:202.35,205.3 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:207.2,209.20 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:209.20,210.53 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:210.53,213.4 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:217.2,222.16 3 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:222.16,225.3 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:227.2,228.49 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:228.49,230.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:233.2,236.16 3 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:236.16,239.3 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:242.2,243.16 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:243.16,246.3 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:249.2,254.16 3 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:254.16,257.3 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:258.2,258.39 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:258.39,261.3 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:262.2,262.33 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:262.33,264.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:265.2,265.34 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:265.34,268.3 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:271.2,272.58 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:272.58,274.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:277.2,278.16 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:278.16,281.3 2 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:282.2,282.15 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:282.15,283.35 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:283.35,285.4 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:289.2,289.39 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:289.39,291.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:292.2,292.32 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:292.32,294.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:297.2,302.12 3 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:305.39,307.50 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:307.50,309.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:310.2,310.16 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:310.16,312.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:314.2,317.16 3 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:317.16,319.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:321.2,321.52 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:321.52,323.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:325.2,325.46 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:325.46,327.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:329.2,334.50 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:334.50,336.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:337.2,337.16 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:337.16,339.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:341.2,342.16 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:342.16,344.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:346.2,348.58 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:348.58,350.3 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:352.2,354.94 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:354.94,356.3 1 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:358.2,360.12 2 1 +github.com/echovault/echovault/internal/snapshot/snapshot.go:363.46,365.2 1 0 +github.com/echovault/echovault/internal/snapshot/snapshot.go:367.42,369.2 1 1 github.com/echovault/echovault/internal/modules/admin/commands.go:27.78,33.29 4 1 github.com/echovault/echovault/internal/modules/admin/commands.go:33.29,34.54 1 1 github.com/echovault/echovault/internal/modules/admin/commands.go:34.54,40.42 4 1 @@ -1527,11 +1041,11 @@ github.com/echovault/echovault/internal/modules/admin/commands.go:181.3,182.26 2 github.com/echovault/echovault/internal/modules/admin/commands.go:183.10,184.54 1 0 github.com/echovault/echovault/internal/modules/admin/commands.go:188.75,190.2 1 0 github.com/echovault/echovault/internal/modules/admin/commands.go:192.36,200.84 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:200.84,204.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:213.84,217.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:200.84,204.5 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:213.84,217.5 1 1 github.com/echovault/echovault/internal/modules/admin/commands.go:225.86,229.7 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:238.86,242.7 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:252.86,256.7 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:238.86,242.7 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:252.86,256.7 1 1 github.com/echovault/echovault/internal/modules/admin/commands.go:267.84,271.5 1 0 github.com/echovault/echovault/internal/modules/admin/commands.go:272.73,273.49 1 0 github.com/echovault/echovault/internal/modules/admin/commands.go:273.49,275.6 1 0 @@ -1561,54 +1075,117 @@ github.com/echovault/echovault/internal/modules/admin/commands.go:380.86,384.7 1 github.com/echovault/echovault/internal/modules/admin/commands.go:385.75,388.38 3 1 github.com/echovault/echovault/internal/modules/admin/commands.go:388.38,390.8 1 1 github.com/echovault/echovault/internal/modules/admin/commands.go:391.7,391.30 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:26.68,28.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:28.16,30.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:32.2,37.16 4 1 -github.com/echovault/echovault/internal/modules/set/commands.go:37.16,39.91 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:39.91,41.4 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:42.3,42.70 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:45.2,46.9 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:46.9,48.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:50.2,52.51 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:55.69,57.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:57.16,59.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:61.2,64.16 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:64.16,66.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:68.2,69.9 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:69.9,71.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:73.2,75.57 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:78.69,80.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:80.16,82.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:84.2,87.34 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:87.34,89.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:91.2,92.9 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:92.9,94.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:96.2,97.41 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:97.41,99.10 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:99.10,100.12 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:102.3,102.27 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:105.2,109.26 4 1 -github.com/echovault/echovault/internal/modules/set/commands.go:109.26,111.24 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:111.24,113.4 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:116.2,116.25 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:119.74,121.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:121.16,123.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:125.2,129.34 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:129.34,131.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:133.2,134.9 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:134.9,136.3 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:138.2,139.40 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:139.40,141.10 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:141.10,142.12 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:144.3,144.27 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:147.2,152.99 4 1 -github.com/echovault/echovault/internal/modules/set/commands.go:152.99,154.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:156.2,156.25 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:159.70,161.16 2 1 -github.com/echovault/echovault/internal/modules/set/commands.go:161.16,163.3 1 0 -github.com/echovault/echovault/internal/modules/set/commands.go:165.2,169.37 3 1 -github.com/echovault/echovault/internal/modules/set/commands.go:169.37,170.14 1 1 -github.com/echovault/echovault/internal/modules/set/commands.go:170.14,172.4 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:24.72,26.16 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:26.16,28.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:30.2,34.9 4 1 +github.com/echovault/echovault/internal/modules/string/commands.go:34.9,36.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:38.2,40.16 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:40.16,42.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:44.2,45.9 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:45.9,47.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:50.2,50.24 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:50.24,52.94 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:52.94,54.4 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:55.3,55.58 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:59.2,59.16 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:59.16,61.94 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:61.94,63.4 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:64.3,64.58 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:67.2,69.35 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:69.35,71.24 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:71.24,74.12 3 1 +github.com/echovault/echovault/internal/modules/string/commands.go:77.3,78.8 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:81.2,81.103 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:81.103,83.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:85.2,85.59 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:88.70,90.16 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:90.16,92.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:94.2,97.16 3 1 +github.com/echovault/echovault/internal/modules/string/commands.go:97.16,99.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:101.2,103.9 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:103.9,105.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:107.2,107.56 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:110.70,112.16 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:112.16,114.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:116.2,123.24 6 1 +github.com/echovault/echovault/internal/modules/string/commands.go:123.24,125.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:127.2,127.16 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:127.16,129.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:131.2,132.9 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:132.9,134.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:136.2,136.15 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:136.15,138.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:139.2,139.13 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:139.13,141.3 1 0 +github.com/echovault/echovault/internal/modules/string/commands.go:143.2,143.30 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:143.30,145.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:147.2,147.22 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:147.22,149.3 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:151.2,151.17 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:151.17,154.3 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:156.2,158.14 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:158.14,160.38 2 1 +github.com/echovault/echovault/internal/modules/string/commands.go:160.38,162.4 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:163.3,163.12 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:166.2,166.65 1 1 +github.com/echovault/echovault/internal/modules/string/commands.go:169.36,209.2 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:23.78,24.19 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:24.19,26.3 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:27.2,31.8 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:34.76,35.19 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:35.19,37.3 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:38.2,42.8 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:45.76,46.19 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:46.19,48.3 1 1 +github.com/echovault/echovault/internal/modules/string/key_funcs.go:49.2,53.8 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:26.68,28.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:28.16,30.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:32.2,37.16 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:37.16,39.91 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:39.91,41.4 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:42.3,42.70 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:45.2,46.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:46.9,48.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:50.2,52.51 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:55.69,57.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:57.16,59.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:61.2,64.16 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:64.16,66.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:68.2,69.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:69.9,71.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:73.2,75.57 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:78.69,80.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:80.16,82.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:84.2,87.34 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:87.34,89.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:91.2,92.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:92.9,94.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:96.2,97.41 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:97.41,99.10 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:99.10,100.12 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:102.3,102.27 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:105.2,109.26 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:109.26,111.24 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:111.24,113.4 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:116.2,116.25 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:119.74,121.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:121.16,123.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:125.2,129.34 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:129.34,131.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:133.2,134.9 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:134.9,136.3 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:138.2,139.40 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:139.40,141.10 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:141.10,142.12 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:144.3,144.27 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:147.2,152.99 4 1 +github.com/echovault/echovault/internal/modules/set/commands.go:152.99,154.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:156.2,156.25 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:159.70,161.16 2 1 +github.com/echovault/echovault/internal/modules/set/commands.go:161.16,163.3 1 0 +github.com/echovault/echovault/internal/modules/set/commands.go:165.2,169.37 3 1 +github.com/echovault/echovault/internal/modules/set/commands.go:169.37,170.14 1 1 +github.com/echovault/echovault/internal/modules/set/commands.go:170.14,172.4 1 1 github.com/echovault/echovault/internal/modules/set/commands.go:173.3,174.10 2 1 github.com/echovault/echovault/internal/modules/set/commands.go:174.10,177.4 1 1 github.com/echovault/echovault/internal/modules/set/commands.go:178.3,178.27 1 1 @@ -2576,271 +2153,558 @@ github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:371.42, github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:373.17,374.26 1 1 github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:375.17,376.46 1 1 github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:377.14,379.46 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:385.3,385.41 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:385.41,386.65 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:386.65,388.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:388.7,390.5 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:392.3,392.30 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:397.74,398.24 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:399.9,400.39 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:401.9,403.52 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:403.52,408.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:409.3,409.30 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:410.9,413.52 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:413.52,415.48 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:415.48,416.13 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:419.4,421.42 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:421.42,423.23 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:424.17,425.26 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:426.17,427.46 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:428.14,430.46 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:437.4,437.34 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:439.3,439.30 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:440.10,446.40 4 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:446.40,447.37 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:447.37,448.13 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:450.4,452.42 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:452.42,453.23 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:454.17,455.26 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:456.17,457.46 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:458.14,460.46 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:466.3,466.30 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:24.97,26.60 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:26.60,28.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:29.2,29.24 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:29.24,30.48 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:30.48,31.85 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:31.85,32.10 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:34.4,35.18 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:35.18,37.5 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:38.4,38.32 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:42.2,43.62 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:43.62,45.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:46.2,46.26 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:46.26,47.94 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:47.94,49.4 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:50.3,50.53 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:53.2,54.63 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:54.63,56.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:57.2,57.27 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:57.27,59.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:62.2,63.85 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:63.85,64.26 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:64.26,65.12 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:67.3,67.31 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:67.31,69.12 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:71.3,71.41 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:71.41,73.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:76.2,77.30 2 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:77.30,79.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:79.8,81.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:83.2,83.55 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:83.55,85.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:85.8,85.31 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:85.31,86.34 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:86.34,88.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:91.2,91.50 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:94.69,95.25 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:95.25,97.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:98.2,100.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:100.9,102.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:103.2,103.69 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:103.69,105.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:106.2,106.20 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:109.65,110.23 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:110.23,112.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:113.2,115.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:115.9,117.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:118.2,118.67 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:118.67,120.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:121.2,121.18 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:124.59,125.20 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:125.20,127.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:128.2,130.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:130.9,132.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:133.2,133.34 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:133.34,135.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:136.2,136.16 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:139.53,140.17 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:140.17,142.3 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:143.2,145.9 3 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:145.9,147.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:148.2,148.35 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:148.35,150.3 1 0 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:151.2,151.15 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:154.61,155.31 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:156.10,157.13 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:158.12,159.16 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:159.16,161.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:162.3,162.13 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:163.12,164.16 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:164.16,166.4 1 1 -github.com/echovault/echovault/internal/modules/sorted_set/utils.go:167.3,167.13 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:24.72,26.16 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:26.16,28.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:30.2,34.9 4 1 -github.com/echovault/echovault/internal/modules/string/commands.go:34.9,36.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:38.2,40.16 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:40.16,42.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:44.2,45.9 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:45.9,47.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:50.2,50.24 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:50.24,52.94 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:52.94,54.4 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:55.3,55.58 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:59.2,59.16 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:59.16,61.94 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:61.94,63.4 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:64.3,64.58 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:67.2,69.35 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:69.35,71.24 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:71.24,74.12 3 1 -github.com/echovault/echovault/internal/modules/string/commands.go:77.3,78.8 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:81.2,81.103 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:81.103,83.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:85.2,85.59 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:88.70,90.16 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:90.16,92.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:94.2,97.16 3 1 -github.com/echovault/echovault/internal/modules/string/commands.go:97.16,99.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:101.2,103.9 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:103.9,105.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:107.2,107.56 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:110.70,112.16 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:112.16,114.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:116.2,123.24 6 1 -github.com/echovault/echovault/internal/modules/string/commands.go:123.24,125.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:127.2,127.16 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:127.16,129.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:131.2,132.9 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:132.9,134.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:136.2,136.15 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:136.15,138.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:139.2,139.13 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:139.13,141.3 1 0 -github.com/echovault/echovault/internal/modules/string/commands.go:143.2,143.30 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:143.30,145.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:147.2,147.22 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:147.22,149.3 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:151.2,151.17 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:151.17,154.3 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:156.2,158.14 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:158.14,160.38 2 1 -github.com/echovault/echovault/internal/modules/string/commands.go:160.38,162.4 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:163.3,163.12 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:166.2,166.65 1 1 -github.com/echovault/echovault/internal/modules/string/commands.go:169.36,209.2 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:23.78,24.19 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:24.19,26.3 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:27.2,31.8 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:34.76,35.19 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:35.19,37.3 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:38.2,42.8 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:45.76,46.19 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:46.19,48.3 1 1 -github.com/echovault/echovault/internal/modules/string/key_funcs.go:49.2,53.8 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:55.56,56.30 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:56.30,58.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:61.59,62.30 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:62.30,64.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:67.64,68.30 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:68.30,70.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:73.59,74.30 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:74.30,76.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:79.59,80.30 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:80.30,82.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:85.60,86.30 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:86.30,88.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:91.82,92.30 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:92.30,94.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:97.77,98.30 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:98.30,100.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:103.73,104.30 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:104.30,106.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:109.89,110.30 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:110.30,112.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:115.65,122.30 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:122.31,122.32 0 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:123.31,123.32 0 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:124.52,126.4 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:127.71,127.72 0 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:128.48,128.49 0 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:129.43,131.4 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:134.2,134.33 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:134.33,136.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:138.2,138.34 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:138.34,139.13 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:139.13,140.8 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:140.8,142.62 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:142.62,143.50 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:143.50,145.7 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:151.2,151.15 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:154.44,174.58 7 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:174.58,177.3 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:180.2,182.16 3 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:182.16,183.37 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:183.37,186.18 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:186.18,189.5 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:190.4,190.24 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:191.9,194.4 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:197.2,198.16 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:198.16,201.3 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:202.2,202.35 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:202.35,205.3 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:207.2,209.20 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:209.20,210.53 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:210.53,213.4 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:217.2,222.16 3 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:222.16,225.3 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:227.2,228.49 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:228.49,230.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:233.2,236.16 3 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:236.16,239.3 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:242.2,243.16 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:243.16,246.3 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:249.2,254.16 3 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:254.16,257.3 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:258.2,258.39 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:258.39,261.3 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:262.2,262.33 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:262.33,264.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:265.2,265.34 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:265.34,268.3 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:271.2,272.58 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:272.58,274.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:277.2,278.16 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:278.16,281.3 2 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:282.2,282.15 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:282.15,283.35 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:283.35,285.4 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:289.2,289.39 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:289.39,291.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:292.2,292.32 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:292.32,294.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:297.2,302.12 3 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:305.39,307.50 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:307.50,309.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:310.2,310.16 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:310.16,312.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:314.2,317.16 3 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:317.16,319.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:321.2,321.52 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:321.52,323.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:325.2,325.46 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:325.46,327.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:329.2,334.50 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:334.50,336.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:337.2,337.16 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:337.16,339.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:341.2,342.16 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:342.16,344.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:346.2,348.58 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:348.58,350.3 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:352.2,354.94 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:354.94,356.3 1 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:358.2,360.12 2 1 -github.com/echovault/echovault/internal/snapshot/snapshot.go:363.46,365.2 1 0 -github.com/echovault/echovault/internal/snapshot/snapshot.go:367.42,369.2 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:385.3,385.41 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:385.41,386.65 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:386.65,388.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:388.7,390.5 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:392.3,392.30 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:397.74,398.24 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:399.9,400.39 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:401.9,403.52 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:403.52,408.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:409.3,409.30 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:410.9,413.52 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:413.52,415.48 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:415.48,416.13 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:419.4,421.42 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:421.42,423.23 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:424.17,425.26 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:426.17,427.46 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:428.14,430.46 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:437.4,437.34 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:439.3,439.30 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:440.10,446.40 4 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:446.40,447.37 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:447.37,448.13 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:450.4,452.42 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:452.42,453.23 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:454.17,455.26 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:456.17,457.46 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:458.14,460.46 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/sorted_set.go:466.3,466.30 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:24.97,26.60 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:26.60,28.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:29.2,29.24 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:29.24,30.48 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:30.48,31.85 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:31.85,32.10 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:34.4,35.18 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:35.18,37.5 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:38.4,38.32 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:42.2,43.62 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:43.62,45.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:46.2,46.26 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:46.26,47.94 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:47.94,49.4 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:50.3,50.53 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:53.2,54.63 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:54.63,56.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:57.2,57.27 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:57.27,59.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:62.2,63.85 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:63.85,64.26 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:64.26,65.12 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:67.3,67.31 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:67.31,69.12 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:71.3,71.41 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:71.41,73.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:76.2,77.30 2 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:77.30,79.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:79.8,81.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:83.2,83.55 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:83.55,85.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:85.8,85.31 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:85.31,86.34 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:86.34,88.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:91.2,91.50 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:94.69,95.25 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:95.25,97.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:98.2,100.9 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:100.9,102.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:103.2,103.69 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:103.69,105.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:106.2,106.20 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:109.65,110.23 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:110.23,112.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:113.2,115.9 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:115.9,117.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:118.2,118.67 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:118.67,120.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:121.2,121.18 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:124.59,125.20 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:125.20,127.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:128.2,130.9 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:130.9,132.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:133.2,133.34 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:133.34,135.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:136.2,136.16 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:139.53,140.17 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:140.17,142.3 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:143.2,145.9 3 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:145.9,147.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:148.2,148.35 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:148.35,150.3 1 0 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:151.2,151.15 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:154.61,155.31 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:156.10,157.13 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:158.12,159.16 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:159.16,161.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:162.3,162.13 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:163.12,164.16 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:164.16,166.4 1 1 +github.com/echovault/echovault/internal/modules/sorted_set/utils.go:167.3,167.13 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:53.62,54.20 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:54.20,56.70 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:56.70,59.4 2 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:61.3,62.17 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:62.17,65.4 2 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:67.3,67.16 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:67.16,68.36 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:68.36,70.5 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:73.3,75.38 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:75.38,76.60 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:76.60,79.5 2 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:82.3,82.71 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:82.71,83.60 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:83.60,86.5 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:92.40,97.24 3 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:97.24,105.3 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:108.2,112.29 3 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:112.29,113.33 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:113.33,115.9 2 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:118.2,118.20 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:118.20,120.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:123.2,123.29 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:123.29,125.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:127.2,137.13 3 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:140.52,145.70 3 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:145.70,147.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:148.2,152.3 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:155.45,161.33 3 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:161.33,162.30 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:162.30,163.47 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:163.47,165.5 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:165.10,168.5 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:172.2,173.45 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:173.45,175.3 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:177.2,184.12 4 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:187.73,192.37 4 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:192.37,193.28 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:193.28,195.12 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:198.3,198.31 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:198.31,199.30 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:199.30,201.5 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:204.3,204.18 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:204.18,205.12 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:208.3,208.52 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:208.52,209.49 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:209.49,211.5 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:214.3,214.63 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:214.63,216.4 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:218.2,218.12 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:221.95,225.19 3 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:225.19,234.60 4 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:234.60,236.4 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:237.3,237.24 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:240.2,240.19 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:240.19,250.31 5 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:250.31,251.28 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:251.28,254.10 3 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:257.3,257.17 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:257.17,259.4 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:263.2,263.19 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:263.19,265.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:268.2,268.21 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:268.21,274.3 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:276.2,276.46 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:276.46,277.38 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:277.38,280.18 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:280.18,287.5 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:291.2,291.50 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:294.131,303.16 6 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:303.16,305.3 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:307.2,311.59 4 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:311.59,315.17 4 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:315.17,317.4 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:321.2,321.36 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:321.36,323.3 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:326.2,326.37 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:326.37,328.3 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:331.2,331.37 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:331.37,333.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:336.2,339.29 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:339.29,341.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:344.2,344.57 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:344.57,346.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:348.2,352.63 3 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:352.63,353.39 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:353.39,355.4 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:356.3,356.63 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:356.63,357.36 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:357.36,359.5 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:361.3,362.26 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:362.26,364.4 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:368.2,368.64 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:368.64,369.101 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:369.101,370.63 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:370.63,373.5 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:374.4,374.16 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:376.5,378.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:381.2,381.94 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:381.94,383.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:383.5,385.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:388.2,388.93 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:388.93,390.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:390.5,392.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:395.2,395.59 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:395.59,397.36 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:397.36,399.106 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:399.106,401.5 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:401.7,403.5 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:405.4,405.105 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:405.105,407.5 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:407.7,409.5 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:411.3,411.13 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:414.2,414.45 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:414.45,416.29 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:416.29,418.4 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:421.3,421.59 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:421.59,422.95 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:422.95,423.49 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:423.49,425.6 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:426.5,426.70 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:426.70,428.6 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:429.5,429.17 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:431.6,432.27 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:432.27,434.5 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:438.3,440.60 3 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:440.60,441.97 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:441.97,442.50 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:442.50,444.6 1 0 +github.com/echovault/echovault/internal/modules/acl/acl.go:445.5,445.70 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:445.70,447.6 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:448.5,448.17 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:450.6,452.4 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:455.2,455.12 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:458.32,462.33 3 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:462.33,467.31 5 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:467.31,468.37 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:468.37,470.5 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:472.3,472.25 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:475.2,475.29 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:475.29,476.33 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:476.33,478.4 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:482.29,484.2 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:486.31,488.2 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:490.30,492.2 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:494.32,496.2 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:498.68,500.31 2 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:500.31,501.13 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:501.13,503.4 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:506.2,506.58 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:506.58,508.3 1 1 +github.com/echovault/echovault/internal/modules/acl/acl.go:509.2,509.19 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:31.68,32.56 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:32.56,34.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:35.2,36.9 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:36.9,38.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:39.2,42.102 3 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:42.102,44.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:45.2,45.42 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:48.67,49.29 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:49.29,51.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:53.2,57.35 3 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:57.35,58.36 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:58.36,59.48 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:59.48,61.5 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:62.4,62.12 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:64.3,64.50 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:64.50,65.51 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:65.51,68.5 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:72.2,72.30 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:72.30,75.34 3 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:75.34,78.4 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:79.3,80.28 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:80.28,82.24 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:82.24,84.5 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:86.3,86.26 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:89.2,89.30 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:89.30,91.46 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:91.46,92.54 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:92.54,94.38 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:94.38,96.30 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:96.30,98.7 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:100.5,100.28 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:105.2,105.85 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:108.71,109.30 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:109.30,111.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:113.2,114.9 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:114.9,116.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:117.2,122.30 5 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:122.30,123.38 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:123.38,126.9 3 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:130.2,130.16 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:130.16,132.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:135.2,139.18 3 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:139.18,141.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:141.8,143.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:144.2,144.21 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:144.21,146.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:147.2,147.17 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:147.17,149.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:151.2,152.29 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:152.29,154.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:157.2,158.51 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:158.51,159.22 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:159.22,161.12 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:163.3,163.49 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:165.2,165.51 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:165.51,166.22 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:166.22,168.12 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:170.3,170.49 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:174.2,175.48 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:175.48,176.21 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:176.21,178.12 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:180.3,180.47 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:182.2,182.48 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:182.48,183.21 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:183.21,185.12 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:187.3,187.47 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:191.2,192.79 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:192.79,193.37 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:193.37,195.4 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:197.2,198.30 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:198.30,199.10 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:200.100,202.53 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:203.53,205.52 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:206.52,208.52 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:213.2,215.54 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:215.54,217.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:218.2,218.54 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:218.54,220.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:222.2,224.25 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:227.69,229.9 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:229.9,231.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:233.2,234.33 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:234.33,236.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:237.2,238.25 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:241.71,243.9 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:243.9,245.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:246.2,246.56 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:246.56,248.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:249.2,249.42 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:252.71,253.29 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:253.29,255.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:256.2,257.9 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:257.9,259.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:260.2,260.75 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:260.75,262.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:263.2,263.42 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:266.70,268.9 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:268.9,270.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:271.2,275.74 4 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:278.68,279.29 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:279.29,281.3 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:282.2,283.9 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:283.9,285.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:286.2,291.33 5 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:291.33,294.19 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:294.19,296.4 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:296.9,298.4 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:300.3,300.22 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:300.22,302.4 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:304.3,304.18 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:304.18,306.4 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:308.3,308.43 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:308.43,309.61 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:309.61,311.5 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:312.4,312.58 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:312.58,314.5 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:317.3,317.52 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:317.52,318.23 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:318.23,320.13 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:322.4,322.39 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:325.3,325.52 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:325.52,326.23 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:326.23,328.13 2 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:330.4,330.39 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:333.3,333.49 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:333.49,334.22 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:334.22,336.13 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:338.4,338.37 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:341.3,341.49 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:341.49,342.22 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:342.22,344.13 2 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:346.4,346.37 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:349.3,349.45 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:349.45,350.52 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:350.52,352.13 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:354.4,354.41 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:357.3,357.46 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:357.46,358.52 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:358.52,360.5 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:363.3,363.55 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:363.55,365.4 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:367.3,367.55 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:367.55,369.4 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:370.3,370.54 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:373.2,374.25 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:377.68,378.30 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:378.30,380.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:382.2,383.9 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:383.9,385.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:386.2,390.16 4 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:390.16,392.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:394.2,394.15 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:394.15,395.35 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:395.35,397.4 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:400.2,404.37 3 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:404.37,405.59 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:405.59,407.4 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:410.2,410.70 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:410.70,411.59 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:411.59,413.4 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:417.2,417.29 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:417.29,421.31 3 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:421.31,422.35 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:422.35,425.54 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:425.54,427.6 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:427.11,430.6 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:431.5,431.10 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:435.3,435.17 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:435.17,437.4 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:440.2,440.42 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:443.68,444.29 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:444.29,446.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:448.2,449.9 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:449.9,451.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:452.2,456.16 4 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:456.16,458.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:460.2,460.15 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:460.15,461.35 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:461.35,463.4 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:466.2,468.37 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:468.37,471.17 2 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:471.17,473.4 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:474.3,474.40 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:474.40,476.4 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:479.2,479.70 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:479.70,482.17 2 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:482.17,484.4 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:485.3,485.40 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:485.40,487.4 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:490.2,490.32 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:490.32,492.3 1 0 +github.com/echovault/echovault/internal/modules/acl/commands.go:494.2,494.42 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:497.36,507.84 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:507.84,513.5 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:522.84,528.5 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:537.86,543.7 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:552.86,558.7 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:567.86,573.7 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:582.86,588.7 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:598.86,604.7 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:613.86,619.7 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:628.86,634.7 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:646.86,652.7 1 1 +github.com/echovault/echovault/internal/modules/acl/commands.go:661.86,667.7 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:53.31,55.39 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:55.39,57.3 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:58.2,59.51 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:59.51,61.3 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:63.2,64.37 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:64.37,66.3 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:67.2,68.49 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:68.49,70.3 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:72.2,73.53 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:73.53,75.3 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:76.2,77.54 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:77.54,79.3 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:81.2,82.43 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:82.43,84.3 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:85.2,86.55 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:86.55,88.3 1 0 +github.com/echovault/echovault/internal/modules/acl/user.go:91.2,91.64 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:91.64,97.3 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:100.79,102.32 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:102.32,103.24 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:103.24,105.12 2 0 +github.com/echovault/echovault/internal/modules/acl/user.go:107.3,107.25 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:109.2,109.33 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:109.33,110.41 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:110.41,113.4 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:114.3,114.17 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:114.17,116.4 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:118.2,118.8 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:121.50,122.26 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:122.26,124.35 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:124.35,126.4 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:127.3,127.36 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:127.36,129.4 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:131.3,131.37 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:131.37,137.12 3 1 +github.com/echovault/echovault/internal/modules/acl/user.go:139.3,139.20 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:139.20,140.84 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:140.84,142.5 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:143.4,143.12 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:145.3,145.20 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:145.20,146.84 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:146.84,148.5 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:149.4,149.12 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:152.3,152.43 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:152.43,155.12 3 1 +github.com/echovault/echovault/internal/modules/acl/user.go:157.3,157.46 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:157.46,159.12 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:161.3,161.36 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:161.36,162.21 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:162.21,164.13 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:166.4,166.21 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:166.21,168.13 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:172.3,172.40 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:172.40,176.12 4 0 +github.com/echovault/echovault/internal/modules/acl/user.go:178.3,178.93 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:178.93,183.12 5 1 +github.com/echovault/echovault/internal/modules/acl/user.go:185.3,185.57 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:185.57,188.12 3 1 +github.com/echovault/echovault/internal/modules/acl/user.go:190.3,190.57 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:190.57,193.12 3 1 +github.com/echovault/echovault/internal/modules/acl/user.go:196.3,196.44 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:196.44,198.12 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:200.3,200.36 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:200.36,201.21 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:201.21,203.13 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:205.4,205.21 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:205.21,207.13 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:211.3,211.44 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:211.44,214.12 3 1 +github.com/echovault/echovault/internal/modules/acl/user.go:216.3,216.66 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:216.66,217.21 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:217.21,219.13 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:221.4,221.21 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:221.21,223.13 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:229.2,229.26 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:229.26,230.39 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:230.39,233.4 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:236.2,236.26 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:236.26,238.42 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:238.42,241.4 2 0 +github.com/echovault/echovault/internal/modules/acl/user.go:243.3,243.43 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:243.43,248.4 4 1 +github.com/echovault/echovault/internal/modules/acl/user.go:250.3,250.60 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:250.60,254.4 3 1 +github.com/echovault/echovault/internal/modules/acl/user.go:256.3,256.46 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:256.46,259.4 2 1 +github.com/echovault/echovault/internal/modules/acl/user.go:262.2,262.12 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:265.36,279.41 12 0 +github.com/echovault/echovault/internal/modules/acl/user.go:279.41,280.65 1 0 +github.com/echovault/echovault/internal/modules/acl/user.go:280.65,282.4 1 0 +github.com/echovault/echovault/internal/modules/acl/user.go:282.6,284.4 1 0 +github.com/echovault/echovault/internal/modules/acl/user.go:287.2,287.18 1 0 +github.com/echovault/echovault/internal/modules/acl/user.go:290.38,303.2 12 1 +github.com/echovault/echovault/internal/modules/acl/user.go:305.40,320.2 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:322.46,323.24 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:323.24,325.3 1 1 +github.com/echovault/echovault/internal/modules/acl/user.go:326.2,326.26 1 1 github.com/echovault/echovault/echovault/api_acl.go:126.71,128.23 2 1 github.com/echovault/echovault/echovault/api_acl.go:128.23,130.3 1 1 github.com/echovault/echovault/echovault/api_acl.go:131.2,132.16 2 1 @@ -3146,56 +3010,54 @@ github.com/echovault/echovault/echovault/api_list.go:294.2,294.41 1 1 github.com/echovault/echovault/echovault/api_list.go:310.76,313.16 3 1 github.com/echovault/echovault/echovault/api_list.go:313.16,315.3 1 1 github.com/echovault/echovault/echovault/api_list.go:316.2,316.41 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:50.86,52.24 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:52.24,54.3 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:56.2,58.36 3 1 -github.com/echovault/echovault/echovault/api_pubsub.go:58.36,65.3 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:65.8,69.3 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:72.2,73.12 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:73.12,75.3 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:77.2,77.25 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:77.25,82.33 4 1 -github.com/echovault/echovault/echovault/api_pubsub.go:82.33,84.4 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:86.3,86.13 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:97.70,98.24 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:98.24,100.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:102.2,102.36 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:102.36,104.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:106.2,107.115 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:120.87,122.24 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:122.24,124.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:126.2,128.36 3 1 -github.com/echovault/echovault/echovault/api_pubsub.go:128.36,135.3 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:135.8,139.3 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:142.2,143.12 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:143.12,145.3 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:147.2,147.25 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:147.25,152.33 4 1 -github.com/echovault/echovault/echovault/api_pubsub.go:152.33,154.4 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:156.3,156.13 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:167.71,168.24 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:168.24,170.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:172.2,172.36 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:172.36,174.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:176.2,177.115 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:190.73,192.16 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:192.16,194.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:195.2,196.40 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:206.75,208.19 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:208.19,210.3 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:211.2,212.16 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:212.16,214.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:215.2,215.45 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:221.54,223.16 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:223.16,225.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:226.2,226.41 1 1 -github.com/echovault/echovault/echovault/api_pubsub.go:236.83,240.16 3 1 +github.com/echovault/echovault/echovault/api_pubsub.go:42.69,46.41 3 1 +github.com/echovault/echovault/echovault/api_pubsub.go:46.41,55.3 4 1 +github.com/echovault/echovault/echovault/api_pubsub.go:55.8,58.10 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:58.10,60.4 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:61.3,62.33 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:65.2,65.33 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:78.95,80.16 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:80.16,81.26 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:81.26,83.4 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:87.2,88.12 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:88.12,90.3 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:92.2,92.25 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:92.25,97.33 4 1 +github.com/echovault/echovault/echovault/api_pubsub.go:97.33,99.4 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:101.3,101.13 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:112.70,114.9 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:114.9,116.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:117.2,118.107 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:131.96,133.16 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:133.16,134.26 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:134.26,136.4 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:140.2,141.12 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:141.12,143.3 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:145.2,145.25 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:145.25,150.33 4 1 +github.com/echovault/echovault/echovault/api_pubsub.go:150.33,152.4 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:154.3,154.13 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:165.71,167.9 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:167.9,169.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:170.2,171.107 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:184.73,186.16 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:186.16,188.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:189.2,190.40 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:200.75,202.19 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:202.19,204.3 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:205.2,206.16 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:206.16,208.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:209.2,209.45 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:215.54,217.16 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:217.16,219.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:220.2,220.41 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:230.83,234.16 3 1 +github.com/echovault/echovault/echovault/api_pubsub.go:234.16,236.3 1 0 +github.com/echovault/echovault/echovault/api_pubsub.go:238.2,240.16 3 1 github.com/echovault/echovault/echovault/api_pubsub.go:240.16,242.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:244.2,246.16 3 1 -github.com/echovault/echovault/echovault/api_pubsub.go:246.16,248.3 1 0 -github.com/echovault/echovault/echovault/api_pubsub.go:250.2,253.28 3 1 -github.com/echovault/echovault/echovault/api_pubsub.go:253.28,256.3 2 1 -github.com/echovault/echovault/echovault/api_pubsub.go:258.2,258.20 1 1 +github.com/echovault/echovault/echovault/api_pubsub.go:244.2,247.28 3 1 +github.com/echovault/echovault/echovault/api_pubsub.go:247.28,250.3 2 1 +github.com/echovault/echovault/echovault/api_pubsub.go:252.2,252.20 1 1 github.com/echovault/echovault/echovault/api_set.go:36.75,39.16 3 1 github.com/echovault/echovault/echovault/api_set.go:39.16,41.3 1 1 github.com/echovault/echovault/echovault/api_set.go:42.2,42.41 1 1 @@ -3458,15 +3320,15 @@ github.com/echovault/echovault/echovault/api_string.go:72.79,74.16 2 1 github.com/echovault/echovault/echovault/api_string.go:74.16,76.3 1 0 github.com/echovault/echovault/echovault/api_string.go:77.2,77.40 1 1 github.com/echovault/echovault/echovault/cluster.go:25.45,27.2 1 1 -github.com/echovault/echovault/echovault/cluster.go:29.84,40.16 4 0 +github.com/echovault/echovault/echovault/cluster.go:29.84,40.16 4 1 github.com/echovault/echovault/echovault/cluster.go:40.16,42.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:44.2,46.43 2 0 +github.com/echovault/echovault/echovault/cluster.go:44.2,46.43 2 1 github.com/echovault/echovault/echovault/cluster.go:46.43,48.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:50.2,52.9 2 0 +github.com/echovault/echovault/echovault/cluster.go:50.2,52.9 2 1 github.com/echovault/echovault/echovault/cluster.go:52.9,54.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:56.2,56.20 1 0 +github.com/echovault/echovault/echovault/cluster.go:56.2,56.20 1 1 github.com/echovault/echovault/echovault/cluster.go:56.20,58.3 1 0 -github.com/echovault/echovault/echovault/cluster.go:60.2,60.12 1 0 +github.com/echovault/echovault/echovault/cluster.go:60.2,60.12 1 1 github.com/echovault/echovault/echovault/cluster.go:63.94,75.16 5 1 github.com/echovault/echovault/echovault/cluster.go:75.16,77.3 1 0 github.com/echovault/echovault/echovault/cluster.go:79.2,81.43 2 1 @@ -3477,331 +3339,337 @@ github.com/echovault/echovault/echovault/cluster.go:91.2,91.20 1 1 github.com/echovault/echovault/echovault/cluster.go:91.20,93.3 1 0 github.com/echovault/echovault/echovault/cluster.go:95.2,95.24 1 1 github.com/echovault/echovault/echovault/config.go:23.36,25.2 1 1 -github.com/echovault/echovault/echovault/echovault.go:111.66,112.36 1 0 -github.com/echovault/echovault/echovault/echovault.go:112.36,114.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:120.66,121.36 1 1 -github.com/echovault/echovault/echovault/echovault.go:121.36,123.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:128.78,136.39 1 1 -github.com/echovault/echovault/echovault/echovault.go:136.39,149.4 12 1 -github.com/echovault/echovault/echovault/echovault.go:152.2,152.33 1 1 -github.com/echovault/echovault/echovault/echovault.go:152.33,154.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:156.2,162.48 2 1 -github.com/echovault/echovault/echovault/echovault.go:162.48,163.52 1 0 -github.com/echovault/echovault/echovault/echovault.go:163.52,165.12 2 0 -github.com/echovault/echovault/echovault/echovault.go:167.3,167.41 1 0 -github.com/echovault/echovault/echovault/echovault.go:171.2,171.52 1 1 -github.com/echovault/echovault/echovault/echovault.go:171.52,173.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:176.2,176.42 1 1 -github.com/echovault/echovault/echovault/echovault.go:176.42,178.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:181.2,182.40 2 1 -github.com/echovault/echovault/echovault/echovault.go:182.40,184.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:187.2,188.43 2 1 -github.com/echovault/echovault/echovault/echovault.go:188.43,190.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:192.2,192.29 1 1 -github.com/echovault/echovault/echovault/echovault.go:192.29,202.38 1 1 -github.com/echovault/echovault/echovault/echovault.go:202.38,206.5 3 0 -github.com/echovault/echovault/echovault/echovault.go:207.49,209.44 2 0 -github.com/echovault/echovault/echovault/echovault.go:209.44,210.46 1 0 -github.com/echovault/echovault/echovault/echovault.go:210.46,212.7 1 0 -github.com/echovault/echovault/echovault/echovault.go:214.5,214.17 1 0 -github.com/echovault/echovault/echovault/echovault.go:217.3,225.5 1 1 -github.com/echovault/echovault/echovault/echovault.go:226.8,237.65 1 1 -github.com/echovault/echovault/echovault/echovault.go:237.65,239.44 2 1 -github.com/echovault/echovault/echovault/echovault.go:239.44,240.46 1 0 -github.com/echovault/echovault/echovault/echovault.go:240.46,242.7 1 0 -github.com/echovault/echovault/echovault/echovault.go:244.5,244.17 1 1 -github.com/echovault/echovault/echovault/echovault.go:246.72,248.93 2 0 -github.com/echovault/echovault/echovault/echovault.go:248.93,250.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:251.5,251.56 1 0 -github.com/echovault/echovault/echovault/echovault.go:255.3,261.60 1 1 -github.com/echovault/echovault/echovault/echovault.go:261.60,263.44 2 0 -github.com/echovault/echovault/echovault/echovault.go:263.44,264.46 1 0 -github.com/echovault/echovault/echovault/echovault.go:264.46,266.7 1 0 -github.com/echovault/echovault/echovault/echovault.go:268.5,268.17 1 0 -github.com/echovault/echovault/echovault/echovault.go:270.68,272.94 2 0 -github.com/echovault/echovault/echovault/echovault.go:272.94,274.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:275.5,275.57 1 0 -github.com/echovault/echovault/echovault/echovault.go:277.51,279.19 2 0 -github.com/echovault/echovault/echovault/echovault.go:279.19,281.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:284.3,284.17 1 1 -github.com/echovault/echovault/echovault/echovault.go:284.17,286.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:287.3,287.34 1 1 -github.com/echovault/echovault/echovault/echovault.go:291.2,291.61 1 1 -github.com/echovault/echovault/echovault/echovault.go:291.61,292.13 1 1 -github.com/echovault/echovault/echovault/echovault.go:292.13,293.8 1 1 -github.com/echovault/echovault/echovault/echovault.go:293.8,295.83 2 1 -github.com/echovault/echovault/echovault/echovault.go:295.83,297.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:302.2,302.69 1 1 -github.com/echovault/echovault/echovault/echovault.go:302.69,304.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:306.2,306.29 1 1 -github.com/echovault/echovault/echovault/echovault.go:306.29,310.36 3 1 -github.com/echovault/echovault/echovault/echovault.go:310.36,312.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:315.2,315.30 1 1 -github.com/echovault/echovault/echovault/echovault.go:315.30,318.34 2 1 -github.com/echovault/echovault/echovault/echovault.go:318.34,320.18 2 0 -github.com/echovault/echovault/echovault/echovault.go:320.18,322.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:326.3,326.71 1 1 -github.com/echovault/echovault/echovault/echovault.go:326.71,328.18 2 0 -github.com/echovault/echovault/echovault/echovault.go:328.18,330.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:334.2,334.23 1 1 -github.com/echovault/echovault/echovault/echovault.go:337.37,346.16 4 1 -github.com/echovault/echovault/echovault/echovault.go:346.16,348.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:350.2,350.15 1 1 -github.com/echovault/echovault/echovault/echovault.go:350.15,353.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:355.2,355.27 1 1 -github.com/echovault/echovault/echovault/echovault.go:355.27,357.15 1 0 -github.com/echovault/echovault/echovault/echovault.go:357.15,359.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:359.9,361.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:363.3,364.49 2 0 -github.com/echovault/echovault/echovault/echovault.go:364.49,366.18 2 0 -github.com/echovault/echovault/echovault/echovault.go:366.18,368.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:369.4,369.42 1 0 -github.com/echovault/echovault/echovault/echovault.go:372.3,375.16 3 0 -github.com/echovault/echovault/echovault/echovault.go:375.16,377.37 2 0 -github.com/echovault/echovault/echovault/echovault.go:377.37,379.19 2 0 -github.com/echovault/echovault/echovault/echovault.go:379.19,381.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:382.5,383.19 2 0 -github.com/echovault/echovault/echovault/echovault.go:383.19,385.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:386.5,386.61 1 0 -github.com/echovault/echovault/echovault/echovault.go:386.61,388.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:392.3,396.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:400.2,400.6 1 1 -github.com/echovault/echovault/echovault/echovault.go:400.6,402.17 2 1 -github.com/echovault/echovault/echovault/echovault.go:402.17,404.12 2 0 -github.com/echovault/echovault/echovault/echovault.go:407.3,407.35 1 1 -github.com/echovault/echovault/echovault/echovault.go:411.58,413.23 1 1 -github.com/echovault/echovault/echovault/echovault.go:413.23,415.3 1 1 -github.com/echovault/echovault/echovault/echovault.go:417.2,423.6 4 1 -github.com/echovault/echovault/echovault/echovault.go:423.6,426.43 2 1 -github.com/echovault/echovault/echovault/echovault.go:426.43,429.9 2 0 -github.com/echovault/echovault/echovault/echovault.go:432.3,432.17 1 1 -github.com/echovault/echovault/echovault/echovault.go:432.17,434.9 2 0 -github.com/echovault/echovault/echovault/echovault.go:437.3,439.43 2 1 -github.com/echovault/echovault/echovault/echovault.go:439.43,440.9 1 1 -github.com/echovault/echovault/echovault/echovault.go:443.3,443.17 1 1 -github.com/echovault/echovault/echovault/echovault.go:443.17,444.87 1 0 -github.com/echovault/echovault/echovault/echovault.go:444.87,446.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:447.4,447.12 1 0 -github.com/echovault/echovault/echovault/echovault.go:450.3,453.20 2 1 -github.com/echovault/echovault/echovault/echovault.go:453.20,454.12 1 0 -github.com/echovault/echovault/echovault/echovault.go:457.3,457.28 1 1 -github.com/echovault/echovault/echovault/echovault.go:457.28,459.12 2 1 -github.com/echovault/echovault/echovault/echovault.go:463.3,464.7 2 0 -github.com/echovault/echovault/echovault/echovault.go:464.7,466.41 1 0 -github.com/echovault/echovault/echovault/echovault.go:466.41,468.19 2 0 -github.com/echovault/echovault/echovault/echovault.go:468.19,470.6 1 0 -github.com/echovault/echovault/echovault/echovault.go:471.5,471.10 1 0 -github.com/echovault/echovault/echovault/echovault.go:473.4,474.21 2 0 -github.com/echovault/echovault/echovault/echovault.go:474.21,475.10 1 0 -github.com/echovault/echovault/echovault/echovault.go:477.4,477.27 1 0 -github.com/echovault/echovault/echovault/echovault.go:481.2,481.37 1 1 -github.com/echovault/echovault/echovault/echovault.go:481.37,483.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:491.34,493.2 1 1 -github.com/echovault/echovault/echovault/echovault.go:496.47,497.38 1 1 -github.com/echovault/echovault/echovault/echovault.go:497.38,499.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:501.2,501.12 1 1 -github.com/echovault/echovault/echovault/echovault.go:501.12,502.27 1 1 -github.com/echovault/echovault/echovault/echovault.go:502.27,504.53 1 0 -github.com/echovault/echovault/echovault/echovault.go:504.53,506.5 1 0 -github.com/echovault/echovault/echovault/echovault.go:507.4,507.10 1 0 -github.com/echovault/echovault/echovault/echovault.go:510.3,510.62 1 1 -github.com/echovault/echovault/echovault/echovault.go:510.62,512.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:515.2,515.12 1 1 -github.com/echovault/echovault/echovault/echovault.go:518.42,520.2 1 1 -github.com/echovault/echovault/echovault/echovault.go:522.43,524.2 1 1 -github.com/echovault/echovault/echovault/echovault.go:526.56,528.2 1 1 -github.com/echovault/echovault/echovault/echovault.go:531.56,533.2 1 1 -github.com/echovault/echovault/echovault/echovault.go:535.44,537.2 1 0 -github.com/echovault/echovault/echovault/echovault.go:539.45,541.2 1 0 -github.com/echovault/echovault/echovault/echovault.go:544.45,545.40 1 0 -github.com/echovault/echovault/echovault/echovault.go:545.40,547.3 1 0 -github.com/echovault/echovault/echovault/echovault.go:548.2,548.12 1 0 -github.com/echovault/echovault/echovault/echovault.go:548.12,549.55 1 0 -github.com/echovault/echovault/echovault/echovault.go:549.55,551.4 1 0 -github.com/echovault/echovault/echovault/echovault.go:553.2,553.12 1 0 -github.com/echovault/echovault/echovault/echovault.go:558.37,559.26 1 0 -github.com/echovault/echovault/echovault/echovault.go:559.26,562.3 2 0 -github.com/echovault/echovault/echovault/echovault.go:565.45,582.2 2 1 +github.com/echovault/echovault/echovault/echovault.go:109.66,110.36 1 1 +github.com/echovault/echovault/echovault/echovault.go:110.36,112.3 1 1 +github.com/echovault/echovault/echovault/echovault.go:118.66,119.36 1 1 +github.com/echovault/echovault/echovault/echovault.go:119.36,121.3 1 1 +github.com/echovault/echovault/echovault/echovault.go:126.78,134.39 1 1 +github.com/echovault/echovault/echovault/echovault.go:134.39,147.4 12 1 +github.com/echovault/echovault/echovault/echovault.go:151.2,151.33 1 1 +github.com/echovault/echovault/echovault/echovault.go:151.33,153.3 1 1 +github.com/echovault/echovault/echovault/echovault.go:155.2,161.48 2 1 +github.com/echovault/echovault/echovault/echovault.go:161.48,162.52 1 0 +github.com/echovault/echovault/echovault/echovault.go:162.52,164.12 2 0 +github.com/echovault/echovault/echovault/echovault.go:166.3,166.41 1 0 +github.com/echovault/echovault/echovault/echovault.go:170.2,175.29 3 1 +github.com/echovault/echovault/echovault/echovault.go:175.29,185.38 1 1 +github.com/echovault/echovault/echovault/echovault.go:185.38,189.5 3 1 +github.com/echovault/echovault/echovault/echovault.go:190.49,192.44 2 0 +github.com/echovault/echovault/echovault/echovault.go:192.44,193.46 1 0 +github.com/echovault/echovault/echovault/echovault.go:193.46,195.7 1 0 +github.com/echovault/echovault/echovault/echovault.go:197.5,197.17 1 0 +github.com/echovault/echovault/echovault/echovault.go:200.3,208.5 1 1 +github.com/echovault/echovault/echovault/echovault.go:209.8,220.65 1 1 +github.com/echovault/echovault/echovault/echovault.go:220.65,222.44 2 1 +github.com/echovault/echovault/echovault/echovault.go:222.44,223.46 1 0 +github.com/echovault/echovault/echovault/echovault.go:223.46,225.7 1 0 +github.com/echovault/echovault/echovault/echovault.go:227.5,227.17 1 1 +github.com/echovault/echovault/echovault/echovault.go:229.72,231.93 2 0 +github.com/echovault/echovault/echovault/echovault.go:231.93,233.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:234.5,234.56 1 0 +github.com/echovault/echovault/echovault/echovault.go:238.3,244.60 1 1 +github.com/echovault/echovault/echovault/echovault.go:244.60,246.44 2 0 +github.com/echovault/echovault/echovault/echovault.go:246.44,247.46 1 0 +github.com/echovault/echovault/echovault/echovault.go:247.46,249.7 1 0 +github.com/echovault/echovault/echovault/echovault.go:251.5,251.17 1 0 +github.com/echovault/echovault/echovault/echovault.go:253.68,255.94 2 0 +github.com/echovault/echovault/echovault/echovault.go:255.94,257.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:258.5,258.57 1 0 +github.com/echovault/echovault/echovault/echovault.go:260.51,262.19 2 0 +github.com/echovault/echovault/echovault/echovault.go:262.19,264.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:267.3,267.17 1 1 +github.com/echovault/echovault/echovault/echovault.go:267.17,269.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:270.3,270.34 1 1 +github.com/echovault/echovault/echovault/echovault.go:274.2,274.61 1 1 +github.com/echovault/echovault/echovault/echovault.go:274.61,275.13 1 0 +github.com/echovault/echovault/echovault/echovault.go:275.13,277.27 2 0 +github.com/echovault/echovault/echovault/echovault.go:277.27,278.83 1 0 +github.com/echovault/echovault/echovault/echovault.go:278.83,280.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:285.2,285.69 1 1 +github.com/echovault/echovault/echovault/echovault.go:285.69,287.3 1 0 +github.com/echovault/echovault/echovault/echovault.go:289.2,289.29 1 1 +github.com/echovault/echovault/echovault/echovault.go:289.29,293.36 3 1 +github.com/echovault/echovault/echovault/echovault.go:293.36,295.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:298.2,298.30 1 1 +github.com/echovault/echovault/echovault/echovault.go:298.30,301.34 2 1 +github.com/echovault/echovault/echovault/echovault.go:301.34,303.18 2 0 +github.com/echovault/echovault/echovault/echovault.go:303.18,305.5 1 0 +github.com/echovault/echovault/echovault/echovault.go:309.3,309.71 1 1 +github.com/echovault/echovault/echovault/echovault.go:309.71,311.18 2 0 +github.com/echovault/echovault/echovault/echovault.go:311.18,313.5 1 0 +github.com/echovault/echovault/echovault/echovault.go:317.2,317.23 1 1 +github.com/echovault/echovault/echovault/echovault.go:320.37,332.16 4 1 +github.com/echovault/echovault/echovault/echovault.go:332.16,335.3 2 0 +github.com/echovault/echovault/echovault/echovault.go:337.2,337.15 1 1 +github.com/echovault/echovault/echovault/echovault.go:337.15,340.3 1 1 +github.com/echovault/echovault/echovault/echovault.go:342.2,342.27 1 1 +github.com/echovault/echovault/echovault/echovault.go:342.27,344.16 1 1 +github.com/echovault/echovault/echovault/echovault.go:344.16,346.4 1 1 +github.com/echovault/echovault/echovault/echovault.go:346.9,348.4 1 1 +github.com/echovault/echovault/echovault/echovault.go:350.3,351.49 2 1 +github.com/echovault/echovault/echovault/echovault.go:351.49,353.18 2 1 +github.com/echovault/echovault/echovault/echovault.go:353.18,356.5 2 0 +github.com/echovault/echovault/echovault/echovault.go:357.4,357.42 1 1 +github.com/echovault/echovault/echovault/echovault.go:360.3,363.16 3 1 +github.com/echovault/echovault/echovault/echovault.go:363.16,365.37 2 1 +github.com/echovault/echovault/echovault/echovault.go:365.37,367.19 2 1 +github.com/echovault/echovault/echovault/echovault.go:367.19,370.6 2 0 +github.com/echovault/echovault/echovault/echovault.go:371.5,372.19 2 1 +github.com/echovault/echovault/echovault/echovault.go:372.19,374.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:375.5,375.61 1 1 +github.com/echovault/echovault/echovault/echovault.go:375.61,377.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:381.3,385.5 1 1 +github.com/echovault/echovault/echovault/echovault.go:388.2,391.6 2 1 +github.com/echovault/echovault/echovault/echovault.go:391.6,392.10 1 1 +github.com/echovault/echovault/echovault/echovault.go:393.22,394.10 1 0 +github.com/echovault/echovault/echovault/echovault.go:395.11,397.18 2 1 +github.com/echovault/echovault/echovault/echovault.go:397.18,400.5 2 1 +github.com/echovault/echovault/echovault/echovault.go:402.4,402.36 1 1 +github.com/echovault/echovault/echovault/echovault.go:407.58,409.23 1 1 +github.com/echovault/echovault/echovault/echovault.go:409.23,411.3 1 1 +github.com/echovault/echovault/echovault/echovault.go:413.2,419.15 4 1 +github.com/echovault/echovault/echovault/echovault.go:419.15,421.38 2 1 +github.com/echovault/echovault/echovault/echovault.go:421.38,423.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:426.2,426.6 1 1 +github.com/echovault/echovault/echovault/echovault.go:426.6,429.43 2 1 +github.com/echovault/echovault/echovault/echovault.go:429.43,432.9 2 0 +github.com/echovault/echovault/echovault/echovault.go:435.3,435.17 1 1 +github.com/echovault/echovault/echovault/echovault.go:435.17,437.9 2 0 +github.com/echovault/echovault/echovault/echovault.go:440.3,441.43 2 1 +github.com/echovault/echovault/echovault/echovault.go:441.43,442.9 1 1 +github.com/echovault/echovault/echovault/echovault.go:444.3,444.17 1 1 +github.com/echovault/echovault/echovault/echovault.go:444.17,445.87 1 1 +github.com/echovault/echovault/echovault/echovault.go:445.87,447.5 1 0 +github.com/echovault/echovault/echovault/echovault.go:448.4,448.12 1 1 +github.com/echovault/echovault/echovault/echovault.go:451.3,454.20 2 1 +github.com/echovault/echovault/echovault/echovault.go:454.20,455.12 1 0 +github.com/echovault/echovault/echovault/echovault.go:458.3,458.28 1 1 +github.com/echovault/echovault/echovault/echovault.go:458.28,460.12 2 1 +github.com/echovault/echovault/echovault/echovault.go:464.3,465.7 2 0 +github.com/echovault/echovault/echovault/echovault.go:465.7,467.41 1 0 +github.com/echovault/echovault/echovault/echovault.go:467.41,469.19 2 0 +github.com/echovault/echovault/echovault/echovault.go:469.19,471.6 1 0 +github.com/echovault/echovault/echovault/echovault.go:472.5,472.10 1 0 +github.com/echovault/echovault/echovault/echovault.go:474.4,475.21 2 0 +github.com/echovault/echovault/echovault/echovault.go:475.21,476.10 1 0 +github.com/echovault/echovault/echovault/echovault.go:478.4,478.27 1 0 +github.com/echovault/echovault/echovault/echovault.go:488.34,490.2 1 1 +github.com/echovault/echovault/echovault/echovault.go:493.47,494.38 1 1 +github.com/echovault/echovault/echovault/echovault.go:494.38,496.3 1 0 +github.com/echovault/echovault/echovault/echovault.go:498.2,498.12 1 1 +github.com/echovault/echovault/echovault/echovault.go:498.12,499.27 1 1 +github.com/echovault/echovault/echovault/echovault.go:499.27,501.53 1 0 +github.com/echovault/echovault/echovault/echovault.go:501.53,503.5 1 0 +github.com/echovault/echovault/echovault/echovault.go:504.4,504.10 1 0 +github.com/echovault/echovault/echovault/echovault.go:507.3,507.62 1 1 +github.com/echovault/echovault/echovault/echovault.go:507.62,509.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:512.2,512.12 1 1 +github.com/echovault/echovault/echovault/echovault.go:515.42,517.2 1 1 +github.com/echovault/echovault/echovault/echovault.go:519.43,521.2 1 1 +github.com/echovault/echovault/echovault/echovault.go:523.56,525.2 1 1 +github.com/echovault/echovault/echovault/echovault.go:528.56,530.2 1 1 +github.com/echovault/echovault/echovault/echovault.go:532.44,534.2 1 0 +github.com/echovault/echovault/echovault/echovault.go:536.45,538.2 1 0 +github.com/echovault/echovault/echovault/echovault.go:541.45,542.40 1 0 +github.com/echovault/echovault/echovault/echovault.go:542.40,544.3 1 0 +github.com/echovault/echovault/echovault/echovault.go:545.2,545.12 1 0 +github.com/echovault/echovault/echovault/echovault.go:545.12,546.55 1 0 +github.com/echovault/echovault/echovault/echovault.go:546.55,548.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:550.2,550.12 1 0 +github.com/echovault/echovault/echovault/echovault.go:555.37,556.35 1 1 +github.com/echovault/echovault/echovault/echovault.go:556.35,557.13 1 1 +github.com/echovault/echovault/echovault/echovault.go:557.13,557.42 1 1 +github.com/echovault/echovault/echovault/echovault.go:558.3,559.71 2 1 +github.com/echovault/echovault/echovault/echovault.go:559.71,561.4 1 0 +github.com/echovault/echovault/echovault/echovault.go:563.2,563.26 1 1 +github.com/echovault/echovault/echovault/echovault.go:563.26,566.3 2 1 +github.com/echovault/echovault/echovault/echovault.go:569.45,586.2 2 1 github.com/echovault/echovault/echovault/keyspace.go:32.67,38.27 4 1 github.com/echovault/echovault/echovault/keyspace.go:38.27,41.3 2 1 github.com/echovault/echovault/echovault/keyspace.go:43.2,43.15 1 1 github.com/echovault/echovault/echovault/keyspace.go:46.58,51.9 4 1 github.com/echovault/echovault/echovault/keyspace.go:51.9,53.3 1 0 github.com/echovault/echovault/echovault/keyspace.go:55.2,55.23 1 1 -github.com/echovault/echovault/echovault/keyspace.go:58.95,66.27 5 1 -github.com/echovault/echovault/echovault/keyspace.go:66.27,68.10 2 1 -github.com/echovault/echovault/echovault/keyspace.go:68.10,70.12 2 1 -github.com/echovault/echovault/echovault/keyspace.go:73.3,73.83 1 1 -github.com/echovault/echovault/echovault/keyspace.go:73.83,74.29 1 0 -github.com/echovault/echovault/echovault/keyspace.go:74.29,77.19 2 0 -github.com/echovault/echovault/echovault/keyspace.go:77.19,79.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:80.10,80.65 1 0 -github.com/echovault/echovault/echovault/keyspace.go:80.65,83.19 2 0 -github.com/echovault/echovault/echovault/keyspace.go:83.19,85.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:86.10,86.66 1 0 -github.com/echovault/echovault/echovault/keyspace.go:86.66,91.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:92.4,93.12 2 0 -github.com/echovault/echovault/echovault/keyspace.go:96.3,96.28 1 1 -github.com/echovault/echovault/echovault/keyspace.go:100.2,100.46 1 1 -github.com/echovault/echovault/echovault/keyspace.go:100.46,101.61 1 1 -github.com/echovault/echovault/echovault/keyspace.go:101.61,103.4 1 0 -github.com/echovault/echovault/echovault/keyspace.go:106.2,106.15 1 1 -github.com/echovault/echovault/echovault/keyspace.go:109.95,113.115 3 1 -github.com/echovault/echovault/echovault/keyspace.go:113.115,115.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:117.2,117.34 1 1 -github.com/echovault/echovault/echovault/keyspace.go:117.34,119.37 2 1 -github.com/echovault/echovault/echovault/keyspace.go:119.37,121.4 1 1 -github.com/echovault/echovault/echovault/keyspace.go:122.3,126.28 2 1 -github.com/echovault/echovault/echovault/keyspace.go:126.28,128.4 1 1 -github.com/echovault/echovault/echovault/keyspace.go:132.2,132.63 1 1 -github.com/echovault/echovault/echovault/keyspace.go:132.63,133.31 1 1 -github.com/echovault/echovault/echovault/keyspace.go:133.31,135.18 2 1 -github.com/echovault/echovault/echovault/keyspace.go:135.18,137.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:141.2,141.12 1 1 -github.com/echovault/echovault/echovault/keyspace.go:144.101,155.55 5 1 -github.com/echovault/echovault/echovault/keyspace.go:155.55,157.3 1 1 -github.com/echovault/echovault/echovault/keyspace.go:158.2,161.11 2 1 -github.com/echovault/echovault/echovault/keyspace.go:161.11,162.44 1 1 -github.com/echovault/echovault/echovault/keyspace.go:162.44,164.18 2 1 -github.com/echovault/echovault/echovault/keyspace.go:164.18,166.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:171.54,178.97 4 1 -github.com/echovault/echovault/echovault/keyspace.go:178.97,180.3 1 1 -github.com/echovault/echovault/echovault/keyspace.go:183.2,183.9 1 1 +github.com/echovault/echovault/echovault/keyspace.go:58.95,64.27 4 1 +github.com/echovault/echovault/echovault/keyspace.go:64.27,66.10 2 1 +github.com/echovault/echovault/echovault/keyspace.go:66.10,68.12 2 1 +github.com/echovault/echovault/echovault/keyspace.go:71.3,71.83 1 1 +github.com/echovault/echovault/echovault/keyspace.go:71.83,72.29 1 0 +github.com/echovault/echovault/echovault/keyspace.go:72.29,75.19 2 0 +github.com/echovault/echovault/echovault/keyspace.go:75.19,77.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:78.10,78.65 1 0 +github.com/echovault/echovault/echovault/keyspace.go:78.65,81.19 2 0 +github.com/echovault/echovault/echovault/keyspace.go:81.19,83.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:84.10,84.66 1 0 +github.com/echovault/echovault/echovault/keyspace.go:84.66,89.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:90.4,91.12 2 0 +github.com/echovault/echovault/echovault/keyspace.go:94.3,94.28 1 1 +github.com/echovault/echovault/echovault/keyspace.go:98.2,98.46 1 1 +github.com/echovault/echovault/echovault/keyspace.go:98.46,99.61 1 1 +github.com/echovault/echovault/echovault/keyspace.go:99.61,101.4 1 0 +github.com/echovault/echovault/echovault/keyspace.go:104.2,104.15 1 1 +github.com/echovault/echovault/echovault/keyspace.go:107.95,111.115 3 1 +github.com/echovault/echovault/echovault/keyspace.go:111.115,113.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:115.2,115.34 1 1 +github.com/echovault/echovault/echovault/keyspace.go:115.34,117.37 2 1 +github.com/echovault/echovault/echovault/keyspace.go:117.37,119.4 1 1 +github.com/echovault/echovault/echovault/keyspace.go:120.3,124.28 2 1 +github.com/echovault/echovault/echovault/keyspace.go:124.28,126.4 1 1 +github.com/echovault/echovault/echovault/keyspace.go:130.2,130.63 1 1 +github.com/echovault/echovault/echovault/keyspace.go:130.63,131.31 1 1 +github.com/echovault/echovault/echovault/keyspace.go:131.31,133.18 2 1 +github.com/echovault/echovault/echovault/keyspace.go:133.18,135.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:139.2,139.12 1 1 +github.com/echovault/echovault/echovault/keyspace.go:142.101,153.55 5 1 +github.com/echovault/echovault/echovault/keyspace.go:153.55,155.3 1 1 +github.com/echovault/echovault/echovault/keyspace.go:156.2,159.11 2 1 +github.com/echovault/echovault/echovault/keyspace.go:159.11,160.44 1 1 +github.com/echovault/echovault/echovault/keyspace.go:160.44,162.18 2 1 +github.com/echovault/echovault/echovault/keyspace.go:162.18,164.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:169.54,176.97 4 1 +github.com/echovault/echovault/echovault/keyspace.go:176.97,178.3 1 1 +github.com/echovault/echovault/echovault/keyspace.go:181.2,181.9 1 1 +github.com/echovault/echovault/echovault/keyspace.go:182.108,183.36 1 0 github.com/echovault/echovault/echovault/keyspace.go:184.108,185.36 1 0 -github.com/echovault/echovault/echovault/keyspace.go:186.108,187.36 1 0 -github.com/echovault/echovault/echovault/keyspace.go:190.2,192.12 2 1 -github.com/echovault/echovault/echovault/keyspace.go:195.60,197.6 1 1 -github.com/echovault/echovault/echovault/keyspace.go:197.6,198.83 1 1 -github.com/echovault/echovault/echovault/keyspace.go:198.83,200.9 2 1 -github.com/echovault/echovault/echovault/keyspace.go:203.2,204.33 2 1 -github.com/echovault/echovault/echovault/keyspace.go:204.33,206.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:207.2,208.13 2 1 -github.com/echovault/echovault/echovault/keyspace.go:213.86,214.27 1 1 -github.com/echovault/echovault/echovault/keyspace.go:214.27,216.84 1 1 -github.com/echovault/echovault/echovault/keyspace.go:216.84,218.4 1 1 -github.com/echovault/echovault/echovault/keyspace.go:220.3,220.35 1 1 -github.com/echovault/echovault/echovault/keyspace.go:220.35,222.4 1 1 -github.com/echovault/echovault/echovault/keyspace.go:223.3,223.56 1 0 -github.com/echovault/echovault/echovault/keyspace.go:224.29,227.34 3 0 -github.com/echovault/echovault/echovault/keyspace.go:228.29,231.34 3 0 -github.com/echovault/echovault/echovault/keyspace.go:232.30,234.51 2 0 -github.com/echovault/echovault/echovault/keyspace.go:234.51,236.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:237.4,237.34 1 0 -github.com/echovault/echovault/echovault/keyspace.go:238.30,240.51 2 0 -github.com/echovault/echovault/echovault/keyspace.go:240.51,242.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:243.4,243.34 1 0 -github.com/echovault/echovault/echovault/keyspace.go:245.3,245.55 1 0 -github.com/echovault/echovault/echovault/keyspace.go:245.55,247.4 1 0 -github.com/echovault/echovault/echovault/keyspace.go:249.2,249.12 1 0 -github.com/echovault/echovault/echovault/keyspace.go:253.71,255.34 1 0 -github.com/echovault/echovault/echovault/keyspace.go:255.34,257.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:260.2,263.50 3 0 -github.com/echovault/echovault/echovault/keyspace.go:263.50,265.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:267.2,269.50 3 0 -github.com/echovault/echovault/echovault/keyspace.go:269.50,271.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:275.2,277.9 3 0 -github.com/echovault/echovault/echovault/keyspace.go:278.125,283.7 3 0 -github.com/echovault/echovault/echovault/keyspace.go:283.7,285.40 1 0 -github.com/echovault/echovault/echovault/keyspace.go:285.40,287.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:289.4,290.29 2 0 -github.com/echovault/echovault/echovault/keyspace.go:290.29,292.49 1 0 -github.com/echovault/echovault/echovault/keyspace.go:292.49,294.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:295.10,295.65 1 0 -github.com/echovault/echovault/echovault/keyspace.go:295.65,297.63 1 0 -github.com/echovault/echovault/echovault/keyspace.go:297.63,299.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:303.4,306.52 3 0 -github.com/echovault/echovault/echovault/keyspace.go:306.52,308.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:310.125,315.7 3 0 -github.com/echovault/echovault/echovault/keyspace.go:315.7,317.40 1 0 -github.com/echovault/echovault/echovault/keyspace.go:317.40,319.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:321.4,322.29 2 0 -github.com/echovault/echovault/echovault/keyspace.go:322.29,324.49 1 0 -github.com/echovault/echovault/echovault/keyspace.go:324.49,326.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:327.10,327.65 1 0 -github.com/echovault/echovault/echovault/keyspace.go:327.65,330.63 1 0 -github.com/echovault/echovault/echovault/keyspace.go:330.63,332.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:336.4,339.52 3 0 -github.com/echovault/echovault/echovault/keyspace.go:339.52,341.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:343.105,346.7 1 0 -github.com/echovault/echovault/echovault/keyspace.go:346.7,349.30 2 0 -github.com/echovault/echovault/echovault/keyspace.go:349.30,353.5 3 0 -github.com/echovault/echovault/echovault/keyspace.go:355.4,356.37 2 0 -github.com/echovault/echovault/echovault/keyspace.go:356.37,357.17 1 0 -github.com/echovault/echovault/echovault/keyspace.go:357.17,358.31 1 0 -github.com/echovault/echovault/echovault/keyspace.go:358.31,360.51 1 0 -github.com/echovault/echovault/echovault/keyspace.go:360.51,362.8 1 0 -github.com/echovault/echovault/echovault/keyspace.go:363.12,363.67 1 0 -github.com/echovault/echovault/echovault/keyspace.go:363.67,364.65 1 0 -github.com/echovault/echovault/echovault/keyspace.go:364.65,366.8 1 0 -github.com/echovault/echovault/echovault/keyspace.go:369.6,372.54 3 0 -github.com/echovault/echovault/echovault/keyspace.go:372.54,374.7 1 0 -github.com/echovault/echovault/echovault/keyspace.go:376.5,376.10 1 0 -github.com/echovault/echovault/echovault/keyspace.go:379.106,382.7 1 0 -github.com/echovault/echovault/echovault/keyspace.go:382.7,389.29 5 0 -github.com/echovault/echovault/echovault/keyspace.go:389.29,391.49 1 0 -github.com/echovault/echovault/echovault/keyspace.go:391.49,393.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:394.10,394.65 1 0 -github.com/echovault/echovault/echovault/keyspace.go:394.65,395.63 1 0 -github.com/echovault/echovault/echovault/keyspace.go:395.63,397.6 1 0 -github.com/echovault/echovault/echovault/keyspace.go:401.4,404.52 3 0 -github.com/echovault/echovault/echovault/keyspace.go:404.52,406.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:408.10,409.13 1 0 -github.com/echovault/echovault/echovault/keyspace.go:418.77,420.57 1 1 -github.com/echovault/echovault/echovault/keyspace.go:420.57,422.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:424.2,429.50 3 1 -github.com/echovault/echovault/echovault/keyspace.go:429.50,431.3 1 0 -github.com/echovault/echovault/echovault/keyspace.go:432.2,439.33 6 1 -github.com/echovault/echovault/echovault/keyspace.go:439.33,440.7 1 0 -github.com/echovault/echovault/echovault/keyspace.go:440.7,444.35 3 0 -github.com/echovault/echovault/echovault/keyspace.go:444.35,446.10 2 0 -github.com/echovault/echovault/echovault/keyspace.go:450.2,455.25 4 1 -github.com/echovault/echovault/echovault/keyspace.go:455.25,458.28 2 0 -github.com/echovault/echovault/echovault/keyspace.go:458.28,459.46 1 0 -github.com/echovault/echovault/echovault/keyspace.go:459.46,461.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:462.9,462.64 1 0 -github.com/echovault/echovault/echovault/keyspace.go:462.64,463.60 1 0 -github.com/echovault/echovault/echovault/keyspace.go:463.60,465.5 1 0 -github.com/echovault/echovault/echovault/keyspace.go:470.2,470.21 1 1 -github.com/echovault/echovault/echovault/keyspace.go:470.21,472.3 1 1 -github.com/echovault/echovault/echovault/keyspace.go:474.2,477.58 2 0 -github.com/echovault/echovault/echovault/keyspace.go:477.58,481.3 2 0 -github.com/echovault/echovault/echovault/keyspace.go:483.2,483.12 1 0 -github.com/echovault/echovault/echovault/modules.go:27.75,30.42 3 1 -github.com/echovault/echovault/echovault/modules.go:30.42,31.46 1 1 -github.com/echovault/echovault/echovault/modules.go:31.46,33.4 1 1 -github.com/echovault/echovault/echovault/modules.go:35.2,35.72 1 1 -github.com/echovault/echovault/echovault/modules.go:38.125,58.37 1 1 -github.com/echovault/echovault/echovault/modules.go:58.37,62.4 3 1 -github.com/echovault/echovault/echovault/modules.go:66.137,68.16 2 1 -github.com/echovault/echovault/echovault/modules.go:68.16,70.3 1 1 -github.com/echovault/echovault/echovault/modules.go:72.2,72.19 1 1 -github.com/echovault/echovault/echovault/modules.go:72.19,74.3 1 0 -github.com/echovault/echovault/echovault/modules.go:76.2,77.16 2 1 -github.com/echovault/echovault/echovault/modules.go:77.16,79.3 1 1 -github.com/echovault/echovault/echovault/modules.go:81.2,85.16 4 1 -github.com/echovault/echovault/echovault/modules.go:85.16,87.3 1 1 -github.com/echovault/echovault/echovault/modules.go:88.2,89.8 2 1 -github.com/echovault/echovault/echovault/modules.go:89.8,92.3 2 1 -github.com/echovault/echovault/echovault/modules.go:94.2,94.51 1 1 -github.com/echovault/echovault/echovault/modules.go:94.51,97.87 1 1 -github.com/echovault/echovault/echovault/modules.go:97.87,99.4 1 0 -github.com/echovault/echovault/echovault/modules.go:103.2,103.50 1 1 -github.com/echovault/echovault/echovault/modules.go:103.50,104.7 1 1 -github.com/echovault/echovault/echovault/modules.go:104.7,105.42 1 1 -github.com/echovault/echovault/echovault/modules.go:105.42,107.10 2 1 -github.com/echovault/echovault/echovault/modules.go:112.2,112.43 1 1 -github.com/echovault/echovault/echovault/modules.go:112.43,114.17 2 1 -github.com/echovault/echovault/echovault/modules.go:114.17,116.4 1 1 -github.com/echovault/echovault/echovault/modules.go:118.3,118.62 1 1 -github.com/echovault/echovault/echovault/modules.go:118.62,120.4 1 1 -github.com/echovault/echovault/echovault/modules.go:122.3,124.18 2 1 -github.com/echovault/echovault/echovault/modules.go:128.2,128.32 1 1 -github.com/echovault/echovault/echovault/modules.go:128.32,131.17 3 1 -github.com/echovault/echovault/echovault/modules.go:131.17,133.4 1 0 -github.com/echovault/echovault/echovault/modules.go:134.3,134.18 1 1 -github.com/echovault/echovault/echovault/modules.go:138.2,138.34 1 0 -github.com/echovault/echovault/echovault/modules.go:138.34,141.3 2 0 -github.com/echovault/echovault/echovault/modules.go:143.2,143.72 1 0 +github.com/echovault/echovault/echovault/keyspace.go:188.2,190.12 2 1 +github.com/echovault/echovault/echovault/keyspace.go:193.60,195.6 1 1 +github.com/echovault/echovault/echovault/keyspace.go:195.6,196.83 1 1 +github.com/echovault/echovault/echovault/keyspace.go:196.83,198.9 2 1 +github.com/echovault/echovault/echovault/keyspace.go:201.2,202.33 2 1 +github.com/echovault/echovault/echovault/keyspace.go:202.33,204.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:205.2,206.13 2 1 +github.com/echovault/echovault/echovault/keyspace.go:211.86,212.27 1 1 +github.com/echovault/echovault/echovault/keyspace.go:212.27,214.84 1 1 +github.com/echovault/echovault/echovault/keyspace.go:214.84,216.4 1 1 +github.com/echovault/echovault/echovault/keyspace.go:218.3,218.35 1 1 +github.com/echovault/echovault/echovault/keyspace.go:218.35,220.4 1 1 +github.com/echovault/echovault/echovault/keyspace.go:221.3,221.56 1 0 +github.com/echovault/echovault/echovault/keyspace.go:222.29,225.34 3 0 +github.com/echovault/echovault/echovault/keyspace.go:226.29,229.34 3 0 +github.com/echovault/echovault/echovault/keyspace.go:230.30,232.51 2 0 +github.com/echovault/echovault/echovault/keyspace.go:232.51,234.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:235.4,235.34 1 0 +github.com/echovault/echovault/echovault/keyspace.go:236.30,238.51 2 0 +github.com/echovault/echovault/echovault/keyspace.go:238.51,240.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:241.4,241.34 1 0 +github.com/echovault/echovault/echovault/keyspace.go:243.3,243.55 1 0 +github.com/echovault/echovault/echovault/keyspace.go:243.55,245.4 1 0 +github.com/echovault/echovault/echovault/keyspace.go:247.2,247.12 1 0 +github.com/echovault/echovault/echovault/keyspace.go:251.71,253.34 1 0 +github.com/echovault/echovault/echovault/keyspace.go:253.34,255.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:258.2,261.50 3 0 +github.com/echovault/echovault/echovault/keyspace.go:261.50,263.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:265.2,267.50 3 0 +github.com/echovault/echovault/echovault/keyspace.go:267.50,269.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:273.2,275.9 3 0 +github.com/echovault/echovault/echovault/keyspace.go:276.125,281.7 3 0 +github.com/echovault/echovault/echovault/keyspace.go:281.7,283.40 1 0 +github.com/echovault/echovault/echovault/keyspace.go:283.40,285.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:287.4,288.29 2 0 +github.com/echovault/echovault/echovault/keyspace.go:288.29,290.49 1 0 +github.com/echovault/echovault/echovault/keyspace.go:290.49,292.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:293.10,293.65 1 0 +github.com/echovault/echovault/echovault/keyspace.go:293.65,295.63 1 0 +github.com/echovault/echovault/echovault/keyspace.go:295.63,297.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:301.4,304.52 3 0 +github.com/echovault/echovault/echovault/keyspace.go:304.52,306.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:308.125,313.7 3 0 +github.com/echovault/echovault/echovault/keyspace.go:313.7,315.40 1 0 +github.com/echovault/echovault/echovault/keyspace.go:315.40,317.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:319.4,320.29 2 0 +github.com/echovault/echovault/echovault/keyspace.go:320.29,322.49 1 0 +github.com/echovault/echovault/echovault/keyspace.go:322.49,324.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:325.10,325.65 1 0 +github.com/echovault/echovault/echovault/keyspace.go:325.65,328.63 1 0 +github.com/echovault/echovault/echovault/keyspace.go:328.63,330.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:334.4,337.52 3 0 +github.com/echovault/echovault/echovault/keyspace.go:337.52,339.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:341.105,344.7 1 0 +github.com/echovault/echovault/echovault/keyspace.go:344.7,347.30 2 0 +github.com/echovault/echovault/echovault/keyspace.go:347.30,351.5 3 0 +github.com/echovault/echovault/echovault/keyspace.go:353.4,354.37 2 0 +github.com/echovault/echovault/echovault/keyspace.go:354.37,355.17 1 0 +github.com/echovault/echovault/echovault/keyspace.go:355.17,356.31 1 0 +github.com/echovault/echovault/echovault/keyspace.go:356.31,358.51 1 0 +github.com/echovault/echovault/echovault/keyspace.go:358.51,360.8 1 0 +github.com/echovault/echovault/echovault/keyspace.go:361.12,361.67 1 0 +github.com/echovault/echovault/echovault/keyspace.go:361.67,362.65 1 0 +github.com/echovault/echovault/echovault/keyspace.go:362.65,364.8 1 0 +github.com/echovault/echovault/echovault/keyspace.go:367.6,370.54 3 0 +github.com/echovault/echovault/echovault/keyspace.go:370.54,372.7 1 0 +github.com/echovault/echovault/echovault/keyspace.go:374.5,374.10 1 0 +github.com/echovault/echovault/echovault/keyspace.go:377.106,380.7 1 0 +github.com/echovault/echovault/echovault/keyspace.go:380.7,387.29 5 0 +github.com/echovault/echovault/echovault/keyspace.go:387.29,389.49 1 0 +github.com/echovault/echovault/echovault/keyspace.go:389.49,391.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:392.10,392.65 1 0 +github.com/echovault/echovault/echovault/keyspace.go:392.65,393.63 1 0 +github.com/echovault/echovault/echovault/keyspace.go:393.63,395.6 1 0 +github.com/echovault/echovault/echovault/keyspace.go:399.4,402.52 3 0 +github.com/echovault/echovault/echovault/keyspace.go:402.52,404.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:406.10,407.13 1 0 +github.com/echovault/echovault/echovault/keyspace.go:416.77,418.57 1 0 +github.com/echovault/echovault/echovault/keyspace.go:418.57,420.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:422.2,427.50 3 0 +github.com/echovault/echovault/echovault/keyspace.go:427.50,429.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:430.2,437.33 6 0 +github.com/echovault/echovault/echovault/keyspace.go:437.33,438.7 1 0 +github.com/echovault/echovault/echovault/keyspace.go:438.7,442.35 3 0 +github.com/echovault/echovault/echovault/keyspace.go:442.35,444.10 2 0 +github.com/echovault/echovault/echovault/keyspace.go:448.2,453.25 4 0 +github.com/echovault/echovault/echovault/keyspace.go:453.25,456.28 2 0 +github.com/echovault/echovault/echovault/keyspace.go:456.28,457.46 1 0 +github.com/echovault/echovault/echovault/keyspace.go:457.46,459.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:460.9,460.64 1 0 +github.com/echovault/echovault/echovault/keyspace.go:460.64,461.60 1 0 +github.com/echovault/echovault/echovault/keyspace.go:461.60,463.5 1 0 +github.com/echovault/echovault/echovault/keyspace.go:468.2,468.21 1 0 +github.com/echovault/echovault/echovault/keyspace.go:468.21,470.3 1 0 +github.com/echovault/echovault/echovault/keyspace.go:472.2,475.58 2 0 +github.com/echovault/echovault/echovault/keyspace.go:475.58,479.3 2 0 +github.com/echovault/echovault/echovault/keyspace.go:481.2,481.12 1 0 +github.com/echovault/echovault/echovault/modules.go:29.75,32.42 3 1 +github.com/echovault/echovault/echovault/modules.go:32.42,33.46 1 1 +github.com/echovault/echovault/echovault/modules.go:33.46,35.4 1 1 +github.com/echovault/echovault/echovault/modules.go:37.2,37.72 1 1 +github.com/echovault/echovault/echovault/modules.go:40.125,60.37 1 1 +github.com/echovault/echovault/echovault/modules.go:60.37,64.4 3 1 +github.com/echovault/echovault/echovault/modules.go:68.137,70.16 2 1 +github.com/echovault/echovault/echovault/modules.go:70.16,72.3 1 1 +github.com/echovault/echovault/echovault/modules.go:74.2,74.19 1 1 +github.com/echovault/echovault/echovault/modules.go:74.19,76.3 1 1 +github.com/echovault/echovault/echovault/modules.go:79.2,79.39 1 1 +github.com/echovault/echovault/echovault/modules.go:79.39,81.3 1 0 +github.com/echovault/echovault/echovault/modules.go:83.2,84.16 2 1 +github.com/echovault/echovault/echovault/modules.go:84.16,86.3 1 1 +github.com/echovault/echovault/echovault/modules.go:88.2,92.16 4 1 +github.com/echovault/echovault/echovault/modules.go:92.16,94.3 1 1 +github.com/echovault/echovault/echovault/modules.go:95.2,96.8 2 1 +github.com/echovault/echovault/echovault/modules.go:96.8,99.3 2 1 +github.com/echovault/echovault/echovault/modules.go:101.2,101.51 1 1 +github.com/echovault/echovault/echovault/modules.go:101.51,104.87 1 1 +github.com/echovault/echovault/echovault/modules.go:104.87,106.4 1 0 +github.com/echovault/echovault/echovault/modules.go:110.2,110.50 1 1 +github.com/echovault/echovault/echovault/modules.go:110.50,111.7 1 1 +github.com/echovault/echovault/echovault/modules.go:111.7,112.42 1 1 +github.com/echovault/echovault/echovault/modules.go:112.42,114.10 2 1 +github.com/echovault/echovault/echovault/modules.go:119.2,119.43 1 1 +github.com/echovault/echovault/echovault/modules.go:119.43,121.17 2 1 +github.com/echovault/echovault/echovault/modules.go:121.17,123.4 1 1 +github.com/echovault/echovault/echovault/modules.go:125.3,125.62 1 1 +github.com/echovault/echovault/echovault/modules.go:125.62,127.4 1 1 +github.com/echovault/echovault/echovault/modules.go:129.3,131.18 2 1 +github.com/echovault/echovault/echovault/modules.go:135.2,135.32 1 1 +github.com/echovault/echovault/echovault/modules.go:135.32,138.17 3 1 +github.com/echovault/echovault/echovault/modules.go:138.17,140.4 1 0 +github.com/echovault/echovault/echovault/modules.go:141.3,141.18 1 1 +github.com/echovault/echovault/echovault/modules.go:145.2,145.34 1 1 +github.com/echovault/echovault/echovault/modules.go:145.34,148.3 2 1 +github.com/echovault/echovault/echovault/modules.go:150.2,150.72 1 1 +github.com/echovault/echovault/echovault/modules.go:153.59,155.2 1 1 +github.com/echovault/echovault/echovault/modules.go:157.47,159.2 1 1 +github.com/echovault/echovault/echovault/modules.go:161.50,163.2 1 1 +github.com/echovault/echovault/echovault/modules.go:165.49,167.2 1 1 github.com/echovault/echovault/echovault/plugin.go:37.72,41.41 3 1 github.com/echovault/echovault/echovault/plugin.go:41.41,42.37 1 1 github.com/echovault/echovault/echovault/plugin.go:42.37,44.4 1 1 @@ -3850,9 +3718,160 @@ github.com/echovault/echovault/echovault/plugin.go:180.42,181.61 1 1 github.com/echovault/echovault/echovault/plugin.go:181.61,183.4 1 1 github.com/echovault/echovault/echovault/plugin.go:183.6,185.4 1 1 github.com/echovault/echovault/echovault/plugin.go:187.2,187.16 1 1 -github.com/echovault/echovault/echovault/test_helpers.go:9.35,16.2 2 1 -github.com/echovault/echovault/echovault/test_helpers.go:18.63,23.2 2 1 -github.com/echovault/echovault/echovault/test_helpers.go:25.95,26.82 1 1 -github.com/echovault/echovault/echovault/test_helpers.go:26.82,28.3 1 0 -github.com/echovault/echovault/echovault/test_helpers.go:29.2,29.12 1 1 -github.com/echovault/echovault/echovault/test_helpers.go:32.95,35.2 2 1 +github.com/echovault/echovault/echovault/test_helpers.go:10.35,18.2 2 1 +github.com/echovault/echovault/echovault/test_helpers.go:20.63,25.2 2 1 +github.com/echovault/echovault/echovault/test_helpers.go:27.95,28.82 1 1 +github.com/echovault/echovault/echovault/test_helpers.go:28.82,30.3 1 0 +github.com/echovault/echovault/echovault/test_helpers.go:31.2,31.12 1 1 +github.com/echovault/echovault/echovault/test_helpers.go:34.95,37.2 2 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:34.51,35.32 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:35.32,37.3 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:41.57,42.32 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:42.32,45.3 2 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:48.61,59.33 3 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:59.33,61.3 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:63.2,63.16 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:66.28,67.12 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:67.12,68.7 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:68.7,73.40 3 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:73.40,74.30 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:74.30,79.21 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:79.21,81.7 1 0 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:85.4,85.33 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:90.34,92.2 1 0 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:94.40,96.2 1 0 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:98.51,101.40 3 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:101.40,103.3 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:104.2,105.11 2 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:108.53,111.40 3 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:111.40,113.3 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:114.2,115.13 2 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:118.44,120.2 1 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:122.36,129.2 4 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:131.34,138.2 4 1 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:140.59,145.35 4 0 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:145.35,147.3 1 0 +github.com/echovault/echovault/internal/modules/pubsub/channel.go:149.2,149.20 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:25.73,27.9 2 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:27.9,29.3 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:31.2,33.24 2 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:33.24,35.3 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:37.2,40.17 3 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:43.75,45.9 2 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:45.9,47.3 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:49.2,53.90 3 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:56.71,58.9 2 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:58.9,60.3 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:61.2,61.30 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:61.30,63.3 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:64.2,65.42 2 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:68.78,69.29 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:69.29,71.3 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:73.2,74.9 2 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:74.9,76.3 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:78.2,79.30 2 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:79.30,81.3 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:83.2,83.38 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:86.76,88.9 2 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:88.9,90.3 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:91.2,92.49 2 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:95.77,97.9 2 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:97.9,99.3 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:100.2,100.47 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:103.36,111.84 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:111.84,113.21 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:113.21,115.6 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:116.5,120.11 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:130.84,132.21 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:132.21,134.6 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:135.5,139.11 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:149.84,151.22 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:151.22,153.6 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:154.5,158.11 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:170.84,177.5 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:188.84,194.5 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:203.84,209.5 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:210.68,212.5 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:222.86,228.7 1 0 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:237.86,243.7 1 1 +github.com/echovault/echovault/internal/modules/pubsub/commands.go:253.86,259.7 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:33.26,38.2 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:40.101,47.17 5 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:47.17,49.3 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:51.2,51.37 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:51.37,55.75 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:55.75,57.4 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:59.3,59.23 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:59.23,62.19 2 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:62.19,64.5 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:64.10,66.5 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:67.4,68.31 2 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:68.31,73.20 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:73.20,75.6 1 0 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:76.5,76.47 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:78.9,80.47 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:80.47,85.20 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:85.20,87.6 1 0 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:93.110,98.17 4 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:98.17,100.3 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:102.2,105.24 3 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:105.24,106.19 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:106.19,109.40 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:109.40,110.31 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:110.31,111.14 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:113.5,113.34 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:113.34,116.6 2 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:118.9,121.40 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:121.40,122.31 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:122.31,123.14 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:125.5,125.34 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:125.34,128.6 2 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:136.2,136.38 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:136.38,137.30 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:137.30,138.54 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:138.54,141.5 2 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:147.2,147.17 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:147.17,148.36 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:148.36,150.40 2 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:150.40,152.58 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:152.58,153.35 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:153.35,156.7 2 0 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:157.6,157.14 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:160.5,160.30 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:160.30,161.35 1 0 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:161.35,164.7 2 0 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:170.2,171.39 2 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:171.39,173.3 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:175.2,175.20 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:178.82,182.38 3 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:182.38,184.29 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:184.29,185.35 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:185.35,187.5 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:188.4,188.12 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:191.3,191.41 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:191.41,193.4 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:197.51,204.19 5 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:204.19,205.39 1 0 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:205.39,206.26 1 0 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:206.26,209.5 2 0 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:211.3,212.21 2 0 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:215.2,217.38 2 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:217.38,219.78 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:219.78,222.12 3 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:225.3,225.50 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:225.50,228.4 2 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:231.2,231.53 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:234.32,239.38 4 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:239.38,240.51 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:240.51,242.4 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:244.2,244.14 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:247.52,252.35 4 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:252.35,254.66 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:254.66,256.4 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:257.3,257.20 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:257.20,259.12 2 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:261.3,261.106 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:263.2,263.20 1 1 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:266.47,271.38 4 0 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:271.38,273.3 1 0 +github.com/echovault/echovault/internal/modules/pubsub/pubsub.go:275.2,275.17 1 0 diff --git a/echovault/api_admin_test.go b/echovault/api_admin_test.go index 749abc3d..889bbd2f 100644 --- a/echovault/api_admin_test.go +++ b/echovault/api_admin_test.go @@ -511,6 +511,7 @@ func TestEchoVault_CommandCount(t *testing.T) { func TestEchoVault_Save(t *testing.T) { conf := DefaultConfig() conf.DataDir = path.Join(".", "testdata", "data") + conf.EvictionPolicy = constants.NoEviction server := createEchoVaultWithConfig(conf) tests := []struct { diff --git a/echovault/echovault.go b/echovault/echovault.go index 1d3b47c5..7a51767f 100644 --- a/echovault/echovault.go +++ b/echovault/echovault.go @@ -50,8 +50,7 @@ import ( type EchoVault struct { // clock is an implementation of a time interface that allows mocking of time functions during testing. - clock clock.Clock - getClock func() clock.Clock + clock clock.Clock // config holds the echovault configuration variables. config config.Config @@ -69,12 +68,12 @@ type EchoVault struct { rwMutex sync.RWMutex // Mutex as only one process should be able to update this list at a time. keys []string // string slice of the volatile keys } - // LFU cache used when eviction policy is allkeys-lfu or volatile-lfu + // LFU cache used when eviction policy is allkeys-lfu or volatile-lfu. lfuCache struct { mutex sync.Mutex // Mutex as only one goroutine can edit the LFU cache at a time. cache eviction.CacheLFU // LFU cache represented by a min head. } - // LRU cache used when eviction policy is allkeys-lru or volatile-lru + // LRU cache used when eviction policy is allkeys-lru or volatile-lru. lruCache struct { mutex sync.Mutex // Mutex as only one goroutine can edit the LRU at a time. cache eviction.CacheLRU // LRU cache represented by a max head. @@ -83,7 +82,6 @@ type EchoVault struct { // Holds the list of all commands supported by the echovault. commandsRWMut sync.RWMutex commands []internal.Command - getCommands func() []internal.Command raft *raft.Raft // The raft replication layer for the echovault. memberList *memberlist.MemberList // The memberlist layer for the echovault. @@ -91,18 +89,18 @@ type EchoVault struct { context context.Context acl *acl.ACL - getACL func() interface{} - - pubSub *pubsub.PubSub - getPubSub func() interface{} + pubSub *pubsub.PubSub snapshotInProgress atomic.Bool // Atomic boolean that's true when actively taking a snapshot. rewriteAOFInProgress atomic.Bool // Atomic boolean that's true when actively rewriting AOF file is in progress. stateCopyInProgress atomic.Bool // Atomic boolean that's true when actively copying state for snapshotting or preamble generation. stateMutationInProgress atomic.Bool // Atomic boolean that is set to true when state mutation is in progress. - latestSnapshotMilliseconds atomic.Int64 // Unix epoch in milliseconds - snapshotEngine *snapshot.Engine // Snapshot engine for standalone mode - aofEngine *aof.Engine // AOF engine for standalone mode + latestSnapshotMilliseconds atomic.Int64 // Unix epoch in milliseconds. + snapshotEngine *snapshot.Engine // Snapshot engine for standalone mode. + aofEngine *aof.Engine // AOF engine for standalone mode. + + listener atomic.Value // Holds the TCP listener. + quit chan struct{} // Channel that signals the closing of all client connections. } // WithContext is an options that for the NewEchoVault function that allows you to @@ -147,6 +145,7 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) { commands = append(commands, str.Commands()...) return commands }(), + quit: make(chan struct{}), } for _, option := range options { @@ -167,27 +166,11 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) { log.Printf("loaded plugin %s\n", path) } - // Function for server commands retrieval - echovault.getCommands = func() []internal.Command { - return echovault.commands - } - - // Function for clock retrieval - echovault.getClock = func() clock.Clock { - return echovault.clock - } - // Set up ACL module echovault.acl = acl.NewACL(echovault.config) - echovault.getACL = func() interface{} { - return echovault.acl - } // Set up Pub/Sub module echovault.pubSub = pubsub.NewPubSub() - echovault.getPubSub = func() interface{} { - return echovault.pubSub - } if echovault.isInCluster() { echovault.raft = raft.NewRaft(raft.Opts{ @@ -290,10 +273,10 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) { // If eviction policy is not noeviction, start a goroutine to evict keys every 100 milliseconds. if echovault.config.EvictionPolicy != constants.NoEviction { go func() { - for { - <-echovault.clock.After(echovault.config.EvictionInterval) + ticker := time.NewTicker(echovault.config.EvictionInterval) + for _ = range ticker.C { if err := echovault.evictKeysWithExpiredTTL(context.Background()); err != nil { - log.Println(err) + log.Printf("evict with ttl: %v\n", err) } } }() @@ -341,30 +324,35 @@ func (server *EchoVault) startTCP() { KeepAlive: 200 * time.Millisecond, } - listener, err := listenConfig.Listen(server.context, "tcp", fmt.Sprintf("%s:%d", conf.BindAddr, conf.Port)) - + listener, err := listenConfig.Listen( + server.context, + "tcp", + fmt.Sprintf("%s:%d", conf.BindAddr, conf.Port), + ) if err != nil { - log.Fatal(err) + log.Printf("listener error: %v", err) + return } if !conf.TLS { // TCP - log.Printf("Starting TCP echovault at Address %s, Port %d...\n", conf.BindAddr, conf.Port) + log.Printf("Starting TCP server at Address %s, Port %d...\n", conf.BindAddr, conf.Port) } if conf.TLS || conf.MTLS { // TLS - if conf.TLS { - log.Printf("Starting mTLS echovault at Address %s, Port %d...\n", conf.BindAddr, conf.Port) + if conf.MTLS { + log.Printf("Starting mTLS server at Address %s, Port %d...\n", conf.BindAddr, conf.Port) } else { - log.Printf("Starting TLS echovault at Address %s, Port %d...\n", conf.BindAddr, conf.Port) + log.Printf("Starting TLS server at Address %s, Port %d...\n", conf.BindAddr, conf.Port) } var certificates []tls.Certificate for _, certKeyPair := range conf.CertKeyPairs { c, err := tls.LoadX509KeyPair(certKeyPair[0], certKeyPair[1]) if err != nil { - log.Fatal(err) + log.Printf("load cert key pair: %v\n", err) + return } certificates = append(certificates, c) } @@ -377,14 +365,15 @@ func (server *EchoVault) startTCP() { for _, c := range conf.ClientCAs { ca, err := os.Open(c) if err != nil { - log.Fatal(err) + log.Printf("client cert open: %v\n", err) + return } certBytes, err := io.ReadAll(ca) if err != nil { - log.Fatal(err) + log.Printf("client cert read: %v\n", err) } if ok := clientCerts.AppendCertsFromPEM(certBytes); !ok { - log.Fatal(err) + log.Printf("client cert append: %v\n", err) } } } @@ -396,15 +385,22 @@ func (server *EchoVault) startTCP() { }) } - // Listen to connection + server.listener.Store(listener) + + // Listen to connection. for { - conn, err := listener.Accept() - if err != nil { - log.Println("Could not establish connection") - continue + select { + case <-server.quit: + return + default: + conn, err := listener.Accept() + if err != nil { + log.Printf("listener error: %v\n", err) + return + } + // Read loop for connection + go server.handleConnection(conn) } - // Read loop for connection - go server.handleConnection(conn) } } @@ -420,6 +416,13 @@ func (server *EchoVault) handleConnection(conn net.Conn) { ctx := context.WithValue(server.context, internal.ContextConnID("ConnectionID"), fmt.Sprintf("%s-%d", server.context.Value(internal.ContextServerID("ServerID")), cid)) + defer func() { + log.Printf("closing connection %d...", cid) + if err := conn.Close(); err != nil { + log.Println(err) + } + }() + for { message, err := internal.ReadMessage(r) @@ -435,11 +438,9 @@ func (server *EchoVault) handleConnection(conn net.Conn) { } res, err := server.handleCommand(ctx, message, &conn, false, false) - if err != nil && errors.Is(err, io.EOF) { break } - if err != nil { if _, err = w.Write([]byte(fmt.Sprintf("-Error %s\r\n", err.Error()))); err != nil { log.Println(err) @@ -449,7 +450,7 @@ func (server *EchoVault) handleConnection(conn net.Conn) { chunkSize := 1024 - // If the length of the response is 0, return nothing to the client + // If the length of the response is 0, return nothing to the client. if len(res) == 0 { continue } @@ -477,10 +478,6 @@ func (server *EchoVault) handleConnection(conn net.Conn) { startIndex += chunkSize } } - - if err := conn.Close(); err != nil { - log.Println(err) - } } // Start starts the EchoVault instance's TCP listener. @@ -556,6 +553,13 @@ func (server *EchoVault) rewriteAOF() error { // ShutDown gracefully shuts down the EchoVault instance. // This function shuts down the memberlist and raft layers. func (server *EchoVault) ShutDown() { + if server.listener.Load() != nil { + go func() { server.quit <- struct{}{} }() + log.Println("closing tcp listener...") + if err := server.listener.Load().(net.Listener).Close(); err != nil { + log.Printf("listener close: %v\n", err) + } + } if server.isInCluster() { server.raft.RaftShutdown() server.memberList.MemberListShutdown() diff --git a/echovault/echovault_test.go b/echovault/echovault_test.go index 66dcabd9..812f9c59 100644 --- a/echovault/echovault_test.go +++ b/echovault/echovault_test.go @@ -1,14 +1,38 @@ +// Copyright 2024 Kelvin Clement Mwinuka +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package echovault import ( + "bufio" + "context" + "crypto/tls" + "crypto/x509" "fmt" "github.com/echovault/echovault/internal" + "github.com/echovault/echovault/internal/config" + "github.com/echovault/echovault/internal/constants" "github.com/tidwall/resp" + "io" "math" "net" + "os" + "path" "strings" "sync" "testing" + "time" ) type ClientServerPair struct { @@ -18,6 +42,9 @@ type ClientServerPair struct { raftPort int mlPort int bootstrapCluster bool + forwardCommand bool + joinAddr string + raw net.Conn client *resp.Conn server *EchoVault } @@ -42,40 +69,92 @@ func getBindAddr() net.IP { return getBindAddrNet(0) } -var setupLock sync.Mutex - func setupServer( serverId string, bootstrapCluster bool, + forwardCommand bool, bindAddr, joinAddr string, port, raftPort, mlPort int, ) (*EchoVault, error) { - setupLock.Lock() - defer setupLock.Unlock() - - config := DefaultConfig() - config.DataDir = "./testdata" - config.BindAddr = bindAddr - config.JoinAddr = joinAddr - config.Port = uint16(port) - config.InMemory = true - config.ServerID = serverId - config.RaftBindPort = uint16(raftPort) - config.MemberListBindPort = uint16(mlPort) - config.BootstrapCluster = bootstrapCluster - return NewEchoVault(WithConfig(config)) + conf := DefaultConfig() + conf.DataDir = "./testdata" + conf.ForwardCommand = forwardCommand + conf.BindAddr = bindAddr + conf.JoinAddr = joinAddr + conf.Port = uint16(port) + conf.InMemory = true + conf.ServerID = serverId + conf.RaftBindPort = uint16(raftPort) + conf.MemberListBindPort = uint16(mlPort) + conf.BootstrapCluster = bootstrapCluster + conf.EvictionPolicy = constants.NoEviction + + return NewEchoVault( + WithContext(context.Background()), + WithConfig(conf), + ) +} + +func setupNode(node *ClientServerPair, isLeader bool, errChan *chan error) { + server, err := setupServer( + node.serverId, + node.bootstrapCluster, + node.forwardCommand, + node.bindAddr, + node.joinAddr, + node.port, + node.raftPort, + node.mlPort, + ) + if err != nil { + *errChan <- fmt.Errorf("could not start server; %v", err) + } + + // Start the server. + go func() { + server.Start() + }() + + if isLeader { + // If node is a leader, wait until it's established itself as a leader of the raft cluster. + for { + if server.raft.IsRaftLeader() { + break + } + } + } else { + // If the node is a follower, wait until it's joined the raft cluster before moving forward. + for { + if server.raft.HasJoinedCluster() { + break + } + } + } + + // Setup client connection. + conn, err := internal.GetConnection(node.bindAddr, node.port) + if err != nil { + *errChan <- fmt.Errorf("could not open tcp connection: %v", err) + } + client := resp.NewConn(conn) + + node.raw = conn + node.client = client + node.server = server } -func MakeCluster(size int) ([]ClientServerPair, error) { +func makeCluster(size int) ([]ClientServerPair, error) { pairs := make([]ClientServerPair, size) + // Set up node metadata. for i := 0; i < len(pairs); i++ { serverId := fmt.Sprintf("SERVER-%d", i) bindAddr := getBindAddr().String() bootstrapCluster := i == 0 + forwardCommand := i < len(pairs)-1 // The last node will not forward commands to the cluster leader. joinAddr := "" if !bootstrapCluster { joinAddr = fmt.Sprintf("%s/%s:%d", pairs[0].serverId, pairs[0].bindAddr, pairs[0].mlPort) @@ -92,48 +171,6 @@ func MakeCluster(size int) ([]ClientServerPair, error) { if err != nil { return nil, fmt.Errorf("could not get free memberlist port: %v", err) } - server, err := setupServer(serverId, bootstrapCluster, bindAddr, joinAddr, port, raftPort, memberlistPort) - if err != nil { - return nil, fmt.Errorf("could not start server; %v", err) - } - - // Start the server - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - server.Start() - }() - wg.Wait() - - if i == 0 { - // If node is a leader, wait until it's established itself as a leader of the raft cluster. - for { - if server.raft.IsRaftLeader() { - break - } - } - } else { - // If the node is a follower, wait until it's joined the raft cluster before moving forward. - for { - if server.raft.HasJoinedCluster() { - break - } - } - } - - // Setup client connection. - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) - if err != nil { - return nil, fmt.Errorf("could not open tcp connection: %v", err) - } - for { - // Wait until connection is no longer nil - if conn != nil { - break - } - } - client := resp.NewConn(conn) pairs[i] = ClientServerPair{ serverId: serverId, @@ -142,83 +179,700 @@ func MakeCluster(size int) ([]ClientServerPair, error) { raftPort: raftPort, mlPort: memberlistPort, bootstrapCluster: bootstrapCluster, - client: client, - server: server, + forwardCommand: forwardCommand, + joinAddr: joinAddr, } } + errChan := make(chan error) + doneChan := make(chan struct{}) + + // Set up nodes. + wg := sync.WaitGroup{} + for i := 0; i < len(pairs); i++ { + if i == 0 { + setupNode(&pairs[i], pairs[i].bootstrapCluster, &errChan) + continue + } + wg.Add(1) + go func(idx int) { + setupNode(&pairs[idx], pairs[idx].bootstrapCluster, &errChan) + wg.Done() + }(i) + } + go func() { + wg.Wait() + doneChan <- struct{}{} + }() + + select { + case err := <-errChan: + return nil, err + case <-doneChan: + } + return pairs, nil } -func Test_ClusterReplication(t *testing.T) { - nodes, err := MakeCluster(5) +func Test_Cluster(t *testing.T) { + nodes, err := makeCluster(5) if err != nil { t.Error(err) return } - // Prepare the write data for the cluster - tests := []struct { + t.Cleanup(func() { + for i := len(nodes) - 1; i > -1; i-- { + _ = nodes[i].raw.Close() + nodes[i].server.ShutDown() + } + }) + + // Prepare the write data for the cluster. + tests := map[string][]struct { key string value string }{ - { - key: "key1", - value: "value1", + "replication": { + {key: "key1", value: "value1"}, + {key: "key2", value: "value2"}, + {key: "key3", value: "value3"}, + }, + "deletion": { + {key: "key4", value: "value4"}, + {key: "key5", value: "value4"}, + {key: "key6", value: "value5"}, }, - { - key: "key2", - value: "value2", + "raft-apply-delete": { + {key: "key7", value: "value7"}, + {key: "key8", value: "value8"}, + {key: "key9", value: "value9"}, }, - { - key: "key3", - value: "value3", + "forward": { + {key: "key10", value: "value10"}, + {key: "key11", value: "value11"}, + {key: "key12", value: "value12"}, }, } - // Write all the data to the cluster leader - for i, test := range tests { - node := nodes[0] - if err := node.client.WriteArray([]resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.value), - }); err != nil { - t.Errorf("could not write data to leader node (test %d): %v", i, err) + t.Run("Test_Replication", func(t *testing.T) { + tests := tests["replication"] + // Write all the data to the cluster leader. + for i, test := range tests { + node := nodes[0] + if err := node.client.WriteArray([]resp.Value{ + resp.StringValue("SET"), resp.StringValue(test.key), resp.StringValue(test.value), + }); err != nil { + t.Errorf("could not write data to leader node (test %d): %v", i, err) + } + // Read response and make sure we received "ok" response. + rd, _, err := node.client.ReadValue() + if err != nil { + t.Errorf("could not read response from leader node (test %d): %v", i, err) + } + if !strings.EqualFold(rd.String(), "ok") { + t.Errorf("expected response for test %d to be \"OK\", got %s", i, rd.String()) + } + } + + // Yield + ticker := time.NewTicker(200 * time.Millisecond) + defer func() { + ticker.Stop() + }() + <-ticker.C + + // Check if the data has been replicated on a quorum (majority of the cluster). + quorum := int(math.Ceil(float64(len(nodes)/2)) + 1) + for i, test := range tests { + count := 0 + for j := 0; j < len(nodes); j++ { + node := nodes[j] + if err := node.client.WriteArray([]resp.Value{ + resp.StringValue("GET"), + resp.StringValue(test.key), + }); err != nil { + t.Errorf("could not write data to follower node %d (test %d): %v", j, i, err) + } + rd, _, err := node.client.ReadValue() + if err != nil { + t.Errorf("could not read data from follower node %d (test %d): %v", j, i, err) + } + if rd.String() == test.value { + count += 1 // If the expected value is found, increment the count. + } + } + // Fail if count is less than quorum. + if count < quorum { + t.Errorf("could not find value %s at key %s in cluster quorum", test.value, test.key) + } + } + }) + + t.Run("Test_DeleteKey", func(t *testing.T) { + tests := tests["deletion"] + // Write all the data to the cluster leader. + for i, test := range tests { + node := nodes[0] + _, ok, err := node.server.Set(test.key, test.value, SetOptions{}) + if err != nil { + t.Errorf("could not write command to leader node (test %d): %v", i, err) + } + if !ok { + t.Errorf("expected set for test %d ok = true, got ok = false", i) + } + } + + // Yield + ticker := time.NewTicker(200 * time.Millisecond) + defer func() { + ticker.Stop() + }() + <-ticker.C + + // Check if the data has been replicated on a quorum (majority of the cluster). + quorum := int(math.Ceil(float64(len(nodes)/2)) + 1) + for i, test := range tests { + count := 0 + for j := 0; j < len(nodes); j++ { + node := nodes[j] + if err := node.client.WriteArray([]resp.Value{ + resp.StringValue("GET"), + resp.StringValue(test.key), + }); err != nil { + t.Errorf("could not write command to follower node %d (test %d): %v", j, i, err) + } + rd, _, err := node.client.ReadValue() + if err != nil { + t.Errorf("could not read data from follower node %d (test %d): %v", j, i, err) + } + if rd.String() == test.value { + count += 1 // If the expected value is found, increment the count. + } + } + // Fail if count is less than quorum. + if count < quorum { + t.Errorf("could not find value %s at key %s in cluster quorum", test.value, test.key) + return + } } - // Read response and make sure we received "ok" response. - rd, _, err := node.client.ReadValue() + + // Delete the key on the leader node + // 1. Prepare delete command. + command := []resp.Value{resp.StringValue("DEL")} + for _, test := range tests { + command = append(command, resp.StringValue(test.key)) + } + // 2. Send delete command. + if err := nodes[0].client.WriteArray(command); err != nil { + t.Error(err) + return + } + res, _, err := nodes[0].client.ReadValue() if err != nil { - t.Errorf("could not read response from leader node (test %d): %v", i, err) + t.Error(err) + return } - if !strings.EqualFold(rd.String(), "ok") { - t.Errorf("expected response for test %d to be \"OK\", got %s", i, rd.String()) + + // 3. Check the delete count is equal to length of tests. + if res.Integer() != len(tests) { + t.Errorf("expected delete response to be %d, got %d", len(tests), res.Integer()) } - } - // Check if the data has been replicated on a quorum (majority of the cluster). - quorum := int(math.Ceil(float64(len(nodes)/2)) + 1) - for i, test := range tests { - count := 0 - for j := 0; j < len(nodes); j++ { - node := nodes[j] + // Yield + ticker.Reset(200 * time.Millisecond) + <-ticker.C + + // 4. Check if the data is absent in quorum (majority of the cluster). + for i, test := range tests { + count := 0 + for j := 0; j < len(nodes); j++ { + node := nodes[j] + if err := node.client.WriteArray([]resp.Value{ + resp.StringValue("GET"), + resp.StringValue(test.key), + }); err != nil { + t.Errorf("could not write command to follower node %d (test %d): %v", j, i, err) + } + rd, _, err := node.client.ReadValue() + if err != nil { + t.Errorf("could not read data from follower node %d (test %d): %v", j, i, err) + } + if rd.IsNull() { + count += 1 // If the expected value is found, increment the count. + } + } + // 5. Fail if count is less than quorum. + if count < quorum { + t.Errorf("could not find value %s at key %s in cluster quorum", test.value, test.key) + } + } + }) + + t.Run("Test_raftApplyDeleteKey", func(t *testing.T) { + tests := tests["raft-apply-delete"] + // Write all the data to the cluster leader. + for i, test := range tests { + node := nodes[0] + _, ok, err := node.server.Set(test.key, test.value, SetOptions{}) + if err != nil { + t.Errorf("could not write command to leader node (test %d): %v", i, err) + } + if !ok { + t.Errorf("expected set for test %d ok = true, got ok = false", i) + } + } + + // Yield + ticker := time.NewTicker(200 * time.Millisecond) + defer func() { + ticker.Stop() + }() + <-ticker.C + + // Check if the data has been replicated on a quorum (majority of the cluster). + quorum := int(math.Ceil(float64(len(nodes)/2)) + 1) + for i, test := range tests { + count := 0 + for j := 0; j < len(nodes); j++ { + node := nodes[j] + if err := node.client.WriteArray([]resp.Value{ + resp.StringValue("GET"), + resp.StringValue(test.key), + }); err != nil { + t.Errorf("could not write command to follower node %d (test %d): %v", j, i, err) + } + rd, _, err := node.client.ReadValue() + if err != nil { + t.Errorf("could not read data from follower node %d (test %d): %v", j, i, err) + } + if rd.String() == test.value { + count += 1 // If the expected value is found, increment the count. + } + } + // Fail if count is less than quorum. + if count < quorum { + t.Errorf("could not find value %s at key %s in cluster quorum", test.value, test.key) + return + } + } + + // Delete the keys using raftApplyDelete method. + for _, test := range tests { + if err := nodes[0].server.raftApplyDeleteKey(nodes[0].server.context, test.key); err != nil { + t.Error(err) + } + } + + // Yield to give key deletion time to take effect across cluster. + ticker.Reset(200 * time.Millisecond) + <-ticker.C + + // Check if the data is absent in quorum (majority of the cluster). + for i, test := range tests { + count := 0 + for j := 0; j < len(nodes); j++ { + node := nodes[j] + if err := node.client.WriteArray([]resp.Value{ + resp.StringValue("GET"), + resp.StringValue(test.key), + }); err != nil { + t.Errorf("could not write command to follower node %d (test %d): %v", j, i, err) + } + rd, _, err := node.client.ReadValue() + if err != nil { + t.Errorf("could not read data from follower node %d (test %d): %v", j, i, err) + } + if rd.IsNull() { + count += 1 // If the expected value is found, increment the count. + } + } + // Fail if count is less than quorum. + if count < quorum { + t.Errorf("found value %s at key %s in cluster quorum", test.value, test.key) + } + } + }) + + t.Run("Test_ForwardCommand", func(t *testing.T) { + tests := tests["forward"] + // Write all the data a random cluster follower. + for i, test := range tests { + // Send write command to follower node. + node := nodes[1] if err := node.client.WriteArray([]resp.Value{ - resp.StringValue("GET"), + resp.StringValue("SET"), resp.StringValue(test.key), + resp.StringValue(test.value), }); err != nil { - t.Errorf("could not write data to follower node %d (test %d): %v", j, i, err) + t.Errorf("could not write data to follower node (test %d): %v", i, err) } + // Read response and make sure we received "ok" response. rd, _, err := node.client.ReadValue() if err != nil { - t.Errorf("could not read data from follower node %d (test %d): %v", j, i, err) + t.Errorf("could not read response from follower node (test %d): %v", i, err) } - if rd.String() == test.value { - count += 1 // If the expected value is found, increment the count. + if !strings.EqualFold(rd.String(), "ok") { + t.Errorf("expected response for test %d to be \"OK\", got %s", i, rd.String()) } } - // Fail if count is less than quorum. - if count < quorum { - t.Errorf("could not find value %s at key %s in cluster quorum", test.value, test.key) + + ticker := time.NewTicker(1 * time.Second) + defer func() { + ticker.Stop() + }() + <-ticker.C + + // Check if the data has been replicated on a quorum (majority of the cluster). + var forwardError error + doneChan := make(chan struct{}) + + go func() { + quorum := int(math.Ceil(float64(len(nodes)/2)) + 1) + for i := 0; i < len(tests); i++ { + test := tests[i] + count := 0 + for j := 0; j < len(nodes); j++ { + node := nodes[j] + if err := node.client.WriteArray([]resp.Value{ + resp.StringValue("GET"), + resp.StringValue(test.key), + }); err != nil { + forwardError = fmt.Errorf("could not write data to follower node %d (test %d): %v", j, i, err) + i = 0 + continue + } + rd, _, err := node.client.ReadValue() + if err != nil { + forwardError = fmt.Errorf("could not read data from follower node %d (test %d): %v", j, i, err) + i = 0 + continue + } + if rd.String() == test.value { + count += 1 // If the expected value is found, increment the count. + } + } + // Fail if count is less than quorum. + if count < quorum { + forwardError = fmt.Errorf("could not find value %s at key %s in cluster quorum", test.value, test.key) + i = 0 + continue + } + } + doneChan <- struct{}{} + }() + + ticker.Reset(5 * time.Second) + + select { + case <-ticker.C: + if forwardError != nil { + t.Errorf("timeout error: %v\n", forwardError) + } + return + case <-doneChan: } + }) + + t.Run("Test_NotLeaderError", func(t *testing.T) { + node := nodes[len(nodes)-1] + err := node.client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue("key"), + resp.StringValue("value"), + }) + if err != nil { + t.Error(err) + return + } + res, _, err := node.client.ReadValue() + if err != nil { + t.Error(err) + return + } + expected := "not cluster leader, cannot carry out command" + if !strings.Contains(res.Error().Error(), expected) { + t.Errorf("expected response to contain \"%s\", got \"%s\"", expected, res.Error().Error()) + } + }) +} + +func Test_Standalone(t *testing.T) { + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return } + + mockServer, err := NewEchoVault( + WithConfig(config.Config{ + BindAddr: "localhost", + Port: uint16(port), + DataDir: "", + EvictionPolicy: constants.NoEviction, + }), + ) + if err != nil { + t.Error(err) + return + } + + go func() { + mockServer.Start() + }() + + t.Cleanup(func() { + mockServer.ShutDown() + }) + + t.Run("Test_EmptyCommand", func(t *testing.T) { + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + if err := client.WriteArray([]resp.Value{}); err != nil { + t.Error(err) + return + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + expected := "empty command" + if !strings.Contains(res.Error().Error(), expected) { + t.Errorf("expcted response to contain \"%s\", got \"%s\"", expected, res.Error().Error()) + } + }) + + t.Run("Test_TLS", func(t *testing.T) { + t.Parallel() + + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + + conf := DefaultConfig() + conf.DataDir = "" + conf.BindAddr = "localhost" + conf.Port = uint16(port) + conf.TLS = true + conf.CertKeyPairs = [][]string{ + { + path.Join("..", "openssl", "server", "server1.crt"), + path.Join("..", "openssl", "server", "server1.key"), + }, + { + path.Join("..", "openssl", "server", "server2.crt"), + path.Join("..", "openssl", "server", "server2.key"), + }, + } + + server, err := NewEchoVault(WithConfig(conf)) + if err != nil { + t.Error(err) + return + } + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + server.Start() + }() + wg.Wait() + + // Dial with ServerCAs + serverCAs := x509.NewCertPool() + f, err := os.Open(path.Join("..", "openssl", "server", "rootCA.crt")) + if err != nil { + t.Error(err) + } + cert, err := io.ReadAll(bufio.NewReader(f)) + if err != nil { + t.Error(err) + } + ok := serverCAs.AppendCertsFromPEM(cert) + if !ok { + t.Error("could not load server CA") + } + + conn, err := internal.GetTLSConnection("localhost", port, &tls.Config{ + RootCAs: serverCAs, + }) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + server.ShutDown() + }() + client := resp.NewConn(conn) + + // Test that we can set and get a value from the server. + key := "key1" + value := "value1" + err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), resp.StringValue(key), resp.StringValue(value), + }) + if err != nil { + t.Error(err) + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected response OK, got \"%s\"", res.String()) + } + + err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}) + if err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if res.String() != value { + t.Errorf("expected response at key \"%s\" to be \"%s\", got \"%s\"", key, value, res.String()) + } + }) + + t.Run("Test_MTLS", func(t *testing.T) { + t.Parallel() + + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + + conf := DefaultConfig() + conf.DataDir = "" + conf.BindAddr = "localhost" + conf.Port = uint16(port) + conf.TLS = true + conf.MTLS = true + conf.ClientCAs = []string{ + path.Join("..", "openssl", "client", "rootCA.crt"), + } + conf.CertKeyPairs = [][]string{ + { + path.Join("..", "openssl", "server", "server1.crt"), + path.Join("..", "openssl", "server", "server1.key"), + }, + { + path.Join("..", "openssl", "server", "server2.crt"), + path.Join("..", "openssl", "server", "server2.key"), + }, + } + + server, err := NewEchoVault(WithConfig(conf)) + if err != nil { + t.Error(err) + return + } + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + server.Start() + }() + wg.Wait() + + // Dial with ServerCAs and client certificates + clientCertKeyPairs := [][]string{ + { + path.Join("..", "openssl", "client", "client1.crt"), + path.Join("..", "openssl", "client", "client1.key"), + }, + { + path.Join("..", "openssl", "client", "client2.crt"), + path.Join("..", "openssl", "client", "client2.key"), + }, + } + var certificates []tls.Certificate + for _, pair := range clientCertKeyPairs { + c, err := tls.LoadX509KeyPair(pair[0], pair[1]) + if err != nil { + t.Error(err) + } + certificates = append(certificates, c) + } + + serverCAs := x509.NewCertPool() + f, err := os.Open(path.Join("..", "openssl", "server", "rootCA.crt")) + if err != nil { + t.Error(err) + } + cert, err := io.ReadAll(bufio.NewReader(f)) + if err != nil { + t.Error(err) + } + ok := serverCAs.AppendCertsFromPEM(cert) + if !ok { + t.Error("could not load server CA") + } + + conn, err := internal.GetTLSConnection("localhost", port, &tls.Config{ + RootCAs: serverCAs, + Certificates: certificates, + }) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + server.ShutDown() + }() + client := resp.NewConn(conn) + + // Test that we can set and get a value from the server. + key := "key1" + value := "value1" + err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), resp.StringValue(key), resp.StringValue(value), + }) + if err != nil { + t.Error(err) + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected response OK, got \"%s\"", res.String()) + } + + err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}) + if err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if res.String() != value { + t.Errorf("expected response at key \"%s\" to be \"%s\", got \"%s\"", key, value, res.String()) + } + }) } diff --git a/echovault/modules.go b/echovault/modules.go index 2d35b0c3..973bea10 100644 --- a/echovault/modules.go +++ b/echovault/modules.go @@ -19,7 +19,9 @@ import ( "errors" "fmt" "github.com/echovault/echovault/internal" + "github.com/echovault/echovault/internal/clock" "github.com/echovault/echovault/internal/constants" + "io" "net" "strings" ) @@ -51,10 +53,10 @@ func (server *EchoVault) getHandlerFuncParams(ctx context.Context, cmd []string, LoadModule: server.LoadModule, UnloadModule: server.UnloadModule, ListModules: server.ListModules, - GetClock: server.getClock, GetPubSub: server.getPubSub, GetACL: server.getACL, GetAllCommands: server.getCommands, + GetClock: server.getClock, DeleteKey: func(key string) error { server.storeLock.Lock() defer server.storeLock.Unlock() @@ -73,6 +75,11 @@ func (server *EchoVault) handleCommand(ctx context.Context, message []byte, conn return nil, errors.New("empty command") } + // If quit command is passed, EOF error. + if strings.EqualFold(cmd[0], "quit") { + return nil, io.EOF + } + command, err := server.getCommand(cmd[0]) if err != nil { return nil, err @@ -142,3 +149,19 @@ func (server *EchoVault) handleCommand(ctx context.Context, message []byte, conn return nil, errors.New("not cluster leader, cannot carry out command") } + +func (server *EchoVault) getCommands() []internal.Command { + return server.commands +} + +func (server *EchoVault) getACL() interface{} { + return server.acl +} + +func (server *EchoVault) getPubSub() interface{} { + return server.pubSub +} + +func (server *EchoVault) getClock() clock.Clock { + return server.clock +} diff --git a/echovault/test_helpers.go b/echovault/test_helpers.go index 2c33a861..4f1b204f 100644 --- a/echovault/test_helpers.go +++ b/echovault/test_helpers.go @@ -4,12 +4,14 @@ import ( "context" "github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal/config" + "github.com/echovault/echovault/internal/constants" ) func createEchoVault() *EchoVault { ev, _ := NewEchoVault( WithConfig(config.Config{ - DataDir: "", + DataDir: "", + EvictionPolicy: constants.NoEviction, }), ) return ev diff --git a/internal/aof/engine_test.go b/internal/aof/engine_test.go index 5548a485..ce806384 100644 --- a/internal/aof/engine_test.go +++ b/internal/aof/engine_test.go @@ -118,7 +118,12 @@ func Test_AOFEngine(t *testing.T) { state[command[1]] = internal.KeyData{Value: command[2], ExpireAt: time.Time{}} engine.QueueCommand(marshalRespCommand(command)) } - <-time.After(100 * time.Millisecond) + ticker := time.NewTicker(100 * time.Millisecond) + defer func() { + ticker.Stop() + }() + + <-ticker.C // Trigger log rewrite if err = engine.RewriteLog(); err != nil { @@ -136,7 +141,9 @@ func Test_AOFEngine(t *testing.T) { state[command[1]] = internal.KeyData{Value: command[2], ExpireAt: time.Time{}} engine.QueueCommand(marshalRespCommand(command)) } - <-time.After(100 * time.Millisecond) + + ticker.Reset(100 * time.Millisecond) + <-ticker.C // Restore logs if err = engine.Restore(); err != nil { diff --git a/internal/aof/log/store_test.go b/internal/aof/log/store_test.go index 92fa0cf1..2866d507 100644 --- a/internal/aof/log/store_test.go +++ b/internal/aof/log/store_test.go @@ -35,6 +35,10 @@ func marshalRespCommand(command []string) []byte { } func Test_AppendStore(t *testing.T) { + t.Cleanup(func() { + _ = os.RemoveAll(path.Join(".", "testdata")) + }) + tests := []struct { name string directory string @@ -133,14 +137,16 @@ func Test_AppendStore(t *testing.T) { done <- struct{}{} }() + ticker := time.NewTicker(200 * time.Millisecond) + defer func() { + ticker.Stop() + }() + select { case <-done: - _ = os.RemoveAll(test.directory) - case <-time.After(200 * time.Millisecond): - _ = os.RemoveAll(test.directory) + case <-ticker.C: t.Error("timeout error") } } - _ = os.RemoveAll("./testdata") } diff --git a/internal/eviction/lru_test.go b/internal/eviction/lru_test.go index 906c924d..f0996fbe 100644 --- a/internal/eviction/lru_test.go +++ b/internal/eviction/lru_test.go @@ -31,10 +31,13 @@ func Test_CacheLRU(t *testing.T) { } access := []string{"key3", "key4", "key1", "key2", "key5"} + ticker := time.NewTicker(200 * time.Millisecond) for _, key := range access { cache.Update(key) - <-time.After(1 * time.Millisecond) + // Yield + <-ticker.C } + ticker.Stop() for i := len(access) - 1; i >= 0; i-- { key := heap.Pop(&cache).(string) diff --git a/internal/memberlist/delegate.go b/internal/memberlist/delegate.go index 4fe6da95..b6673ae3 100644 --- a/internal/memberlist/delegate.go +++ b/internal/memberlist/delegate.go @@ -66,9 +66,8 @@ func (delegate *Delegate) NodeMeta(limit int) []byte { // NotifyMsg implements Delegate interface func (delegate *Delegate) NotifyMsg(msgBytes []byte) { var msg BroadcastMessage - if err := json.Unmarshal(msgBytes, &msg); err != nil { - fmt.Print(err) + log.Printf("notifymsg: %v", err) return } diff --git a/internal/memberlist/memberlist.go b/internal/memberlist/memberlist.go index 4b5e4981..bf258041 100644 --- a/internal/memberlist/memberlist.go +++ b/internal/memberlist/memberlist.go @@ -60,7 +60,7 @@ func NewMemberList(opts Opts) *MemberList { } func (m *MemberList) MemberListInit(ctx context.Context) { - cfg := memberlist.DefaultLANConfig() + cfg := memberlist.DefaultWANConfig() cfg.RequireNodeNames = true cfg.Name = m.options.Config.ServerID cfg.BindAddr = m.options.Config.BindAddr @@ -160,13 +160,15 @@ func (m *MemberList) MemberListShutdown() { // Gracefully leave memberlist cluster err := m.memberList.Leave(500 * time.Millisecond) if err != nil { - log.Fatal("Could not gracefully leave memberlist cluster") + log.Printf("memberlist leave: %v\n", err) + return } err = m.memberList.Shutdown() if err != nil { - log.Fatal("Could not gracefully shutdown memberlist background maintenance") + log.Printf("memberlist shutdown: %v\n", err) + return } - log.Println("Successfully shutdown memberlist") + log.Println("successfully shutdown memberlist") } diff --git a/internal/modules/acl/acl.go b/internal/modules/acl/acl.go index 4cf20bdb..87252a5d 100644 --- a/internal/modules/acl/acl.go +++ b/internal/modules/acl/acl.go @@ -17,6 +17,7 @@ package acl import ( "context" "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -49,6 +50,45 @@ type ACL struct { GlobPatterns map[string]glob.Glob } +func loadUsersFromConfigFile(users []*User, filePath string) { + if filePath != "" { + // Create the director if it does not exist. + if err := os.MkdirAll(path.Dir(filePath), os.ModePerm); err != nil { + log.Printf("mkdir ACL config: %v\n", err) + return + } + // Open the config file. Create it if it does not exist. + f, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, os.ModePerm) + if err != nil { + log.Printf("open ACL config: %v\n", err) + return + } + + defer func() { + if err := f.Close(); err != nil { + log.Printf("close ACL config: %v\n", err) + } + }() + + ext := path.Ext(f.Name()) + + if strings.ToLower(ext) == ".json" { + if err := json.NewDecoder(f).Decode(&users); err != nil { + log.Printf("load ACL config: %v\n", err) + return + } + } + + if slices.Contains([]string{".yaml", ".yml"}, strings.ToLower(ext)) { + if err := yaml.NewDecoder(f).Decode(&users); err != nil { + log.Printf("load ACL config: %v\n", err) + return + } + } + + } +} + func NewACL(config config.Config) *ACL { var users []*User @@ -65,32 +105,7 @@ func NewACL(config config.Config) *ACL { } // 2. Read and parse the ACL config file - if config.AclConfig != "" { - // Override acl configurations from file - if f, err := os.Open(config.AclConfig); err != nil { - panic(err) - } else { - defer func() { - if err := f.Close(); err != nil { - log.Println("acl config file close error: ", err) - } - }() - - ext := path.Ext(f.Name()) - - if ext == ".json" { - if err := json.NewDecoder(f).Decode(&users); err != nil { - log.Fatal("could not load JSON ACL config: ", err) - } - } - - if ext == ".yaml" || ext == ".yml" { - if err := yaml.NewDecoder(f).Decode(&users); err != nil { - log.Fatal("could not load YAML ACL config: ", err) - } - } - } - } + loadUsersFromConfigFile(users, config.AclConfig) // 3. If default user was not loaded from file, add the created one defaultLoaded := false @@ -169,13 +184,6 @@ func (acl *ACL) SetUser(cmd []string) error { return nil } -func (acl *ACL) AddUsers(users []*User) { - acl.LockUsers() - defer acl.UnlockUsers() - - acl.Users = append(acl.Users, users...) -} - func (acl *ACL) DeleteUser(_ context.Context, usernames []string) error { acl.LockUsers() defer acl.UnlockUsers() @@ -211,20 +219,16 @@ func (acl *ACL) DeleteUser(_ context.Context, usernames []string) error { } func (acl *ACL) AuthenticateConnection(_ context.Context, conn *net.Conn, cmd []string) error { - acl.RLockUsers() - defer acl.RUnlockUsers() - var passwords []Password var user *User - h := sha256.New() - if len(cmd) == 2 { // Process AUTH + h := sha256.New() h.Write([]byte(cmd[1])) passwords = []Password{ - {PasswordType: "plaintext", PasswordValue: cmd[1]}, - {PasswordType: "SHA256", PasswordValue: string(h.Sum(nil))}, + {PasswordType: PasswordPlainText, PasswordValue: cmd[1]}, + {PasswordType: PasswordSHA256, PasswordValue: hex.EncodeToString(h.Sum(nil))}, } // Authenticate with default user idx := slices.IndexFunc(acl.Users, func(user *User) bool { @@ -235,10 +239,11 @@ func (acl *ACL) AuthenticateConnection(_ context.Context, conn *net.Conn, cmd [] if len(cmd) == 3 { // Process AUTH + h := sha256.New() h.Write([]byte(cmd[2])) passwords = []Password{ - {PasswordType: "plaintext", PasswordValue: cmd[2]}, - {PasswordType: "SHA256", PasswordValue: string(h.Sum(nil))}, + {PasswordType: PasswordPlainText, PasswordValue: cmd[2]}, + {PasswordType: PasswordSHA256, PasswordValue: hex.EncodeToString(h.Sum(nil))}, } // Find user with the specified username userFound := false @@ -270,10 +275,10 @@ func (acl *ACL) AuthenticateConnection(_ context.Context, conn *net.Conn, cmd [] for _, userPassword := range user.Passwords { for _, password := range passwords { - if strings.EqualFold(userPassword.PasswordType, password.PasswordType) && + if userPassword.PasswordType == password.PasswordType && userPassword.PasswordValue == password.PasswordValue && user.Enabled { - // Set the current connection to the selected user and set them as authenticated + // Set the current connection to the selected user and set them as authenticated. acl.Connections[conn] = Connection{ Authenticated: true, User: user, @@ -317,8 +322,8 @@ func (acl *ACL) AuthorizeConnection(conn *net.Conn, cmd []string, command intern return nil } - // Skip connection - if strings.EqualFold(comm, "connection") { + // Skip PING + if strings.EqualFold(comm, "ping") { return nil } @@ -340,21 +345,23 @@ func (acl *ACL) AuthorizeConnection(conn *net.Conn, cmd []string, command intern return errors.New("user must be authenticated") } - // 2. Check if all categories are in IncludedCategories var notAllowed []string - if !slices.ContainsFunc(categories, func(category string) bool { - return slices.ContainsFunc(connection.User.IncludedCategories, func(includedCategory string) bool { - if includedCategory == "*" || includedCategory == category { - return true + + // 2. Check if all categories are in IncludedCategories + count := make(map[string]int, len(categories)) + if !slices.Contains(connection.User.IncludedCategories, "*") { + for _, category := range categories { + count[category] = 0 + } + for _, category := range connection.User.IncludedCategories { + if _, ok := count[category]; ok { + count[category] += 1 } - notAllowed = append(notAllowed, fmt.Sprintf("@%s", category)) - return false - }) - }) { - if len(notAllowed) == 0 { - notAllowed = []string{"@all"} } - return fmt.Errorf("unauthorized access to the following categories: %+v", notAllowed) + notAllowed = getUnauthorized(count, "@") + if len(notAllowed) > 0 { + return fmt.Errorf("unauthorized access to the following categories: %+v", notAllowed) + } } // 3. Check if commands category is in ExcludedCategories @@ -374,14 +381,14 @@ func (acl *ACL) AuthorizeConnection(conn *net.Conn, cmd []string, command intern if !slices.ContainsFunc(connection.User.IncludedCommands, func(includedCommand string) bool { return includedCommand == "*" || includedCommand == comm }) { - return fmt.Errorf("not authorised to run %s command", comm) + return fmt.Errorf("not authorised to run %s command", strings.ToUpper(comm)) } // 5. Check if command are in ExcludedCommands if slices.ContainsFunc(connection.User.ExcludedCommands, func(excludedCommand string) bool { return excludedCommand == "*" || excludedCommand == comm }) { - return fmt.Errorf("not authorised to run %s command", comm) + return fmt.Errorf("not authorised to run %s command", strings.ToUpper(comm)) } // 6. PUBSUB authorisation. @@ -416,24 +423,32 @@ func (acl *ACL) AuthorizeConnection(conn *net.Conn, cmd []string, command intern if acl.GlobPatterns[readKeyGlob].Match(key) { return true } - notAllowed = append(notAllowed, fmt.Sprintf("%s~%s", "%R", key)) + if !slices.Contains(notAllowed, fmt.Sprintf("%s~%s", "%R", key)) { + notAllowed = append(notAllowed, fmt.Sprintf("%s~%s", "%R", key)) + } return false }) }) { - return fmt.Errorf("not authorised to access the following keys %+v", notAllowed) + if len(notAllowed) > 0 { + return fmt.Errorf("not authorised to access the following keys: %+v", notAllowed) + } } - // 9. Check if keys are in IncludedWriteKeys + // 9. Check if write keys are in IncludedWriteKeys + fmt.Println("KEYS: ", writeKeys) + fmt.Println("ALLOWED KEYS: ", connection.User.IncludedWriteKeys) if !slices.ContainsFunc(writeKeys, func(key string) bool { return slices.ContainsFunc(connection.User.IncludedWriteKeys, func(writeKeyGlob string) bool { if acl.GlobPatterns[writeKeyGlob].Match(key) { return true } - notAllowed = append(notAllowed, fmt.Sprintf("%s~%s", "%W", key)) + if !slices.Contains(notAllowed, fmt.Sprintf("%s~%s", "%W", key)) { + notAllowed = append(notAllowed, fmt.Sprintf("%s~%s", "%W", key)) + } return false }) }) { - return fmt.Errorf("not authorised to access the following keys %+v", notAllowed) + return fmt.Errorf("not authorised to access the following keys: %+v", notAllowed) } } @@ -479,3 +494,17 @@ func (acl *ACL) RLockUsers() { func (acl *ACL) RUnlockUsers() { acl.UsersMutex.RUnlock() } + +func getUnauthorized(count map[string]int, prefix string) []string { + var notAllowed []string + for member, c := range count { + if c == 0 { + notAllowed = append(notAllowed, fmt.Sprintf("%s%s", prefix, member)) + } + } + // Sort the members in alphabetical order. + slices.SortStableFunc(notAllowed, func(a, b string) int { + return internal.CompareLex(a, b) + }) + return notAllowed +} diff --git a/internal/modules/acl/commands.go b/internal/modules/acl/commands.go index 432b596e..cc87c476 100644 --- a/internal/modules/acl/commands.go +++ b/internal/modules/acl/commands.go @@ -36,12 +36,75 @@ func handleAuth(params internal.HandlerFuncParams) ([]byte, error) { if !ok { return nil, errors.New("could not load ACL") } + acl.LockUsers() + defer acl.UnlockUsers() + if err := acl.AuthenticateConnection(params.Context, params.Connection, params.Command); err != nil { return nil, err } return []byte(constants.OkResponse), nil } +func handleCat(params internal.HandlerFuncParams) ([]byte, error) { + if len(params.Command) > 3 { + return nil, errors.New(constants.WrongArgsResponse) + } + + categories := make(map[string][]string) + + commands := params.GetAllCommands() + + for _, command := range commands { + if len(command.SubCommands) == 0 { + for _, category := range command.Categories { + categories[category] = append(categories[category], command.Command) + } + continue + } + for _, subcommand := range command.SubCommands { + for _, category := range subcommand.Categories { + categories[category] = append(categories[category], + fmt.Sprintf("%s|%s", command.Command, subcommand.Command)) + } + } + } + + if len(params.Command) == 2 { + var cats []string + length := 0 + for key, _ := range categories { + cats = append(cats, key) + length += 1 + } + res := fmt.Sprintf("*%d", length) + for i, cat := range cats { + res = fmt.Sprintf("%s\r\n+%s", res, cat) + if i == len(cats)-1 { + res = res + "\r\n" + } + } + return []byte(res), nil + } + + if len(params.Command) == 3 { + var res string + for category, commands := range categories { + if strings.EqualFold(category, params.Command[2]) { + res = fmt.Sprintf("*%d", len(commands)) + for i, command := range commands { + res = fmt.Sprintf("%s\r\n+%s", res, command) + if i == len(commands)-1 { + res = res + "\r\n" + } + } + return []byte(res), nil + } + } + } + + return nil, fmt.Errorf("category %s not found", strings.ToUpper(params.Command[2])) +} + func handleGetUser(params internal.HandlerFuncParams) ([]byte, error) { if len(params.Command) != 3 { return nil, errors.New(constants.WrongArgsResponse) @@ -51,6 +114,8 @@ func handleGetUser(params internal.HandlerFuncParams) ([]byte, error) { if !ok { return nil, errors.New("could not load ACL") } + acl.RLockUsers() + defer acl.RUnlockUsers() var user *User userFound := false @@ -159,71 +224,12 @@ func handleGetUser(params internal.HandlerFuncParams) ([]byte, error) { return []byte(res), nil } -func handleCat(params internal.HandlerFuncParams) ([]byte, error) { - if len(params.Command) > 3 { - return nil, errors.New(constants.WrongArgsResponse) - } - - categories := make(map[string][]string) - - commands := params.GetAllCommands() - - for _, command := range commands { - if len(command.SubCommands) == 0 { - for _, category := range command.Categories { - categories[category] = append(categories[category], command.Command) - } - continue - } - for _, subcommand := range command.SubCommands { - for _, category := range subcommand.Categories { - categories[category] = append(categories[category], - fmt.Sprintf("%s|%s", command.Command, subcommand.Command)) - } - } - } - - if len(params.Command) == 2 { - var cats []string - length := 0 - for key, _ := range categories { - cats = append(cats, key) - length += 1 - } - res := fmt.Sprintf("*%d", length) - for i, cat := range cats { - res = fmt.Sprintf("%s\r\n+%s", res, cat) - if i == len(cats)-1 { - res = res + "\r\n" - } - } - return []byte(res), nil - } - - if len(params.Command) == 3 { - var res string - for category, commands := range categories { - if strings.EqualFold(category, params.Command[2]) { - res = fmt.Sprintf("*%d", len(commands)) - for i, command := range commands { - res = fmt.Sprintf("%s\r\n+%s", res, command) - if i == len(commands)-1 { - res = res + "\r\n" - } - } - return []byte(res), nil - } - } - } - - return nil, fmt.Errorf("category %s not found", strings.ToUpper(params.Command[2])) -} - func handleUsers(params internal.HandlerFuncParams) ([]byte, error) { acl, ok := params.GetACL().(*ACL) if !ok { return nil, errors.New("could not load ACL") } + res := fmt.Sprintf("*%d", len(acl.Users)) for _, user := range acl.Users { res += fmt.Sprintf("\r\n$%d\r\n%s", len(user.Username), user.Username) @@ -262,6 +268,9 @@ func handleWhoAmI(params internal.HandlerFuncParams) ([]byte, error) { if !ok { return nil, errors.New("could not load ACL") } + acl.RLockUsers() + defer acl.RUnlockUsers() + connectionInfo := acl.Connections[params.Connection] return []byte(fmt.Sprintf("+%s\r\n", connectionInfo.User.Username)), nil } @@ -274,6 +283,9 @@ func handleList(params internal.HandlerFuncParams) ([]byte, error) { if !ok { return nil, errors.New("could not load ACL") } + acl.RLockUsers() + defer acl.RUnlockUsers() + res := fmt.Sprintf("*%d", len(acl.Users)) s := "" for _, user := range acl.Users { @@ -342,7 +354,7 @@ func handleList(params internal.HandlerFuncParams) ([]byte, error) { s += fmt.Sprintf(" %s~%s", "%R", key) } // Included write keys - for _, key := range user.IncludedReadKeys { + for _, key := range user.IncludedWriteKeys { if !slices.Contains(user.IncludedReadKeys, key) { s += fmt.Sprintf(" %s~%s", "%W", key) } @@ -371,11 +383,10 @@ func handleLoad(params internal.HandlerFuncParams) ([]byte, error) { if !ok { return nil, errors.New("could not load ACL") } - acl.LockUsers() defer acl.UnlockUsers() - f, err := os.Open(acl.Config.AclConfig) + f, err := os.OpenFile(acl.Config.AclConfig, os.O_RDONLY, os.ModePerm) if err != nil { return nil, err } @@ -390,13 +401,13 @@ func handleLoad(params internal.HandlerFuncParams) ([]byte, error) { var users []*User - if ext == ".json" { + if strings.ToLower(ext) == ".json" { if err := json.NewDecoder(f).Decode(&users); err != nil { return nil, err } } - if ext == ".yaml" || ext == ".yml" { + if slices.Contains([]string{".yaml", ".yml"}, strings.ToLower(ext)) { if err := yaml.NewDecoder(f).Decode(&users); err != nil { return nil, err } @@ -420,7 +431,7 @@ func handleLoad(params internal.HandlerFuncParams) ([]byte, error) { break } } - // If the no user with current loaded username is already in acl list, then append the user to the list + // If there is no user with current loaded username is already in acl list, then append the user to the list if !userFound { acl.Users = append(acl.Users, user) } @@ -438,11 +449,10 @@ func handleSave(params internal.HandlerFuncParams) ([]byte, error) { if !ok { return nil, errors.New("could not load ACL") } - acl.RLockUsers() - acl.RUnlockUsers() + defer acl.RUnlockUsers() - f, err := os.OpenFile(acl.Config.AclConfig, os.O_WRONLY|os.O_CREATE, os.ModeAppend) + f, err := os.OpenFile(acl.Config.AclConfig, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm) if err != nil { return nil, err } @@ -455,32 +465,29 @@ func handleSave(params internal.HandlerFuncParams) ([]byte, error) { ext := path.Ext(f.Name()) - if ext == ".json" { + if strings.ToLower(ext) == ".json" { // Write to JSON config file out, err := json.Marshal(acl.Users) if err != nil { return nil, err } - _, err = f.Write(out) - if err != nil { + if _, err = f.Write(out); err != nil { return nil, err } } - if ext == ".yaml" || ext == ".yml" { + if slices.Contains([]string{".yaml", ".yml"}, strings.ToLower(ext)) { // Write to yaml file out, err := yaml.Marshal(acl.Users) if err != nil { return nil, err } - _, err = f.Write(out) - if err != nil { + if _, err = f.Write(out); err != nil { return nil, err } } - err = f.Sync() - if err != nil { + if err = f.Sync(); err != nil { return nil, err } diff --git a/internal/modules/acl/commands_test.go b/internal/modules/acl/commands_test.go index ec2a1d99..1ad74661 100644 --- a/internal/modules/acl/commands_test.go +++ b/internal/modules/acl/commands_test.go @@ -16,47 +16,25 @@ package acl_test import ( "crypto/sha256" + "encoding/hex" "fmt" "github.com/echovault/echovault/echovault" "github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/constants" - "github.com/echovault/echovault/internal/modules/acl" "github.com/tidwall/resp" - "net" - "reflect" + "os" + "path" "slices" "strings" "sync" "testing" - "unsafe" ) -var bindAddr string -var port uint16 -var mockServer *echovault.EchoVault - -func init() { - bindAddr = "localhost" - - p, _ := internal.GetFreePort() - port = uint16(p) - - mockServer = setUpServer(bindAddr, port, true, "") - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() - }() - wg.Wait() -} - -func setUpServer(bindAddr string, port uint16, requirePass bool, aclConfig string) *echovault.EchoVault { +func setUpServer(port int, requirePass bool, aclConfig string) (*echovault.EchoVault, error) { conf := config.Config{ - BindAddr: bindAddr, - Port: port, + BindAddr: "localhost", + Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, RequirePass: requirePass, @@ -64,57 +42,47 @@ func setUpServer(bindAddr string, port uint16, requirePass bool, aclConfig strin AclConfig: aclConfig, } - mockServer, _ := echovault.NewEchoVault( + mockServer, err := echovault.NewEchoVault( echovault.WithConfig(conf), ) - - // Add the initial test users to the ACL module - a := getACL(mockServer) - a.AddUsers(generateInitialTestUsers()) - - return mockServer -} - -func getUnexportedField(field reflect.Value) interface{} { - return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() -} - -func getACL(mockServer *echovault.EchoVault) *acl.ACL { - method := getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getACL")) - f := method.(func() interface{}) - return f().(*acl.ACL) -} - -func generateInitialTestUsers() []*acl.User { - // User with both hash password and plaintext password - withPasswordUser := acl.CreateUser("with_password_user") - h := sha256.New() - h.Write([]byte("password3")) - withPasswordUser.Passwords = []acl.Password{ - {PasswordType: acl.PasswordPlainText, PasswordValue: "password2"}, - {PasswordType: acl.PasswordSHA256, PasswordValue: string(h.Sum(nil))}, + if err != nil { + return nil, err } - withPasswordUser.IncludedCategories = []string{"*"} - withPasswordUser.IncludedCommands = []string{"*"} - // User with NoPassword option - noPasswordUser := acl.CreateUser("no_password_user") - noPasswordUser.Passwords = []acl.Password{ - {PasswordType: acl.PasswordPlainText, PasswordValue: "password4"}, + // Add the initial test users to the ACL module. + for _, user := range generateInitialTestUsers() { + if _, err := mockServer.ACLSetUser(user); err != nil { + return nil, err + } } - noPasswordUser.NoPassword = true - // Disabled user - disabledUser := acl.CreateUser("disabled_user") - disabledUser.Passwords = []acl.Password{ - {PasswordType: acl.PasswordPlainText, PasswordValue: "password5"}, - } - disabledUser.Enabled = false + return mockServer, nil +} - return []*acl.User{ - withPasswordUser, - noPasswordUser, - disabledUser, +func generateInitialTestUsers() []echovault.User { + return []echovault.User{ + { + // User with both hash password and plaintext password. + Username: "with_password_user", + Enabled: true, + IncludeCategories: []string{"*"}, + IncludeCommands: []string{"*"}, + AddPlainPasswords: []string{"password2"}, + AddHashPasswords: []string{generateSHA256Password("password3")}, + }, + { + // User with NoPassword option. + Username: "no_password_user", + Enabled: true, + NoPassword: true, + AddPlainPasswords: []string{"password4"}, + }, + { + // Disabled user. + Username: "disabled_user", + Enabled: false, + AddPlainPasswords: []string{"password5"}, + }, } } @@ -142,47 +110,36 @@ func compareSlices[T comparable](res, expected []T) error { } // compareUsers compares 2 users and checks if all their fields are equal -func compareUsers(user1, user2 *acl.User) error { +func compareUsers(user1, user2 map[string][]string) error { // Compare flags - if user1.Username != user2.Username { - return fmt.Errorf("mismatched usernames \"%s\", and \"%s\"", user1.Username, user2.Username) + if user1["username"][0] != user2["username"][0] { + return fmt.Errorf("mismatched usernames \"%s\", and \"%s\"", user1["username"][0], user2["username"][0]) } - if user1.Enabled != user2.Enabled { - return fmt.Errorf("mismatched enabled flag \"%+v\", and \"%+v\"", user1.Enabled, user2.Enabled) - } - if user1.NoPassword != user2.NoPassword { - return fmt.Errorf("mismatched nopassword flag \"%+v\", and \"%+v\"", user1.NoPassword, user2.NoPassword) - } - if user1.NoKeys != user2.NoKeys { - return fmt.Errorf("mismatched nokeys flag \"%+v\", and \"%+v\"", user1.NoKeys, user2.NoKeys) + + // Check if both users are enabled. + if slices.Contains(user1["flags"], "on") != slices.Contains(user2["flags"], "on") { + return fmt.Errorf("mismatched enabled flag \"%+v\", and \"%+v\"", + slices.Contains(user1["flags"], "on"), slices.Contains(user2["flags"], "on")) } - // Compare passwords - for _, password1 := range user1.Passwords { - if !slices.ContainsFunc(user2.Passwords, func(password2 acl.Password) bool { - return password1.PasswordType == password2.PasswordType && password1.PasswordValue == password2.PasswordValue - }) { - return fmt.Errorf("found password %+v in user1 that was not found in user2", password1) - } + // Check if "nokeys" is present + if slices.Contains(user1["flags"], "nokeys") != slices.Contains(user2["flags"], "nokeys") { + return fmt.Errorf("mismatched nokeys flag \"%+v\", and \"%+v\"", + slices.Contains(user1["flags"], "nokeys"), slices.Contains(user2["flags"], "nokeys")) } - for _, password2 := range user2.Passwords { - if !slices.ContainsFunc(user1.Passwords, func(password1 acl.Password) bool { - return password1.PasswordType == password2.PasswordType && password1.PasswordValue == password2.PasswordValue - }) { - return fmt.Errorf("found password %+v in user2 that was not found in user1", password2) - } + + // Check if "nopass" is present + if slices.Contains(user1["flags"], "nopass") != slices.Contains(user1["flags"], "nopass") { + return fmt.Errorf("mismatched nopassword flag \"%+v\", and \"%+v\"", + slices.Contains(user1["flags"], "nopass"), slices.Contains(user1["flags"], "nopass")) } // Compare permissions permissions := [][][]string{ - {user1.IncludedCategories, user2.IncludedCategories}, - {user1.ExcludedCategories, user2.ExcludedCategories}, - {user1.IncludedCommands, user2.IncludedCommands}, - {user1.ExcludedCommands, user2.ExcludedCommands}, - {user1.IncludedReadKeys, user2.IncludedReadKeys}, - {user1.IncludedWriteKeys, user2.IncludedWriteKeys}, - {user1.IncludedPubSubChannels, user2.IncludedPubSubChannels}, - {user1.ExcludedPubSubChannels, user2.ExcludedPubSubChannels}, + {user1["categories"], user2["categories"]}, + {user1["commands"], user2["commands"]}, + {user1["keys"], user2["keys"]}, + {user1["channels"], user2["channels"]}, } for _, p := range permissions { if err := compareSlices(p[0], p[1]); err != nil { @@ -196,1315 +153,2076 @@ func compareUsers(user1, user2 *acl.User) error { func generateSHA256Password(plain string) string { h := sha256.New() h.Write([]byte(plain)) - return string(h.Sum(nil)) + return hex.EncodeToString(h.Sum(nil)) } -func Test_HandleAuth(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) +func Test_ACL(t *testing.T) { + port, err := internal.GetFreePort() if err != nil { t.Error(err) + return } - for { - // Wait until connection is not nil before breaking out. - if conn != nil { - break - } + mockServer, err := setUpServer(port, true, "") + if err != nil { + t.Error(err) + return } - - defer func() { - if conn != nil { - _ = conn.Close() - } + go func() { + mockServer.Start() }() - r := resp.NewConn(conn) - - tests := []struct { - cmd []resp.Value - wantRes string - wantErr string - }{ - { // 1. Authenticate with default user without specifying username - cmd: []resp.Value{resp.StringValue("AUTH"), resp.StringValue("password1")}, - wantRes: "OK", - wantErr: "", - }, - { // 2. Authenticate with plaintext password - cmd: []resp.Value{ - resp.StringValue("AUTH"), - resp.StringValue("with_password_user"), - resp.StringValue("password2"), - }, - wantRes: "OK", - wantErr: "", - }, - { // 3. Authenticate with SHA256 password - cmd: []resp.Value{ - resp.StringValue("AUTH"), - resp.StringValue("with_password_user"), - resp.StringValue("password3"), - }, - wantRes: "OK", - wantErr: "", - }, - { // 4. Authenticate with no password user - cmd: []resp.Value{ - resp.StringValue("AUTH"), - resp.StringValue("no_password_user"), - resp.StringValue("password4"), - }, - wantRes: "OK", - wantErr: "", - }, - { // 5. Fail to authenticate with disabled user - cmd: []resp.Value{ - resp.StringValue("AUTH"), - resp.StringValue("disabled_user"), - resp.StringValue("password5"), - }, - wantRes: "", - wantErr: "Error user disabled_user is disabled", - }, - { // 6. Fail to authenticate with non-existent user - cmd: []resp.Value{ - resp.StringValue("AUTH"), - resp.StringValue("non_existent_user"), - resp.StringValue("password6"), - }, - wantRes: "", - wantErr: "Error no user with username non_existent_user", - }, - { // 7. Command too short - cmd: []resp.Value{resp.StringValue("AUTH")}, - wantRes: "", - wantErr: fmt.Sprintf("Error %s", constants.WrongArgsResponse), - }, - { // 8. Command too long - cmd: []resp.Value{ - resp.StringValue("AUTH"), - resp.StringValue("user"), - resp.StringValue("password1"), - resp.StringValue("password2"), - }, - wantRes: "", - wantErr: fmt.Sprintf("Error %s", constants.WrongArgsResponse), - }, - } + t.Cleanup(func() { + mockServer.ShutDown() + }) + + t.Run("Test_HandleAuth", func(t *testing.T) { + t.Parallel() - for _, test := range tests { - if err = r.WriteArray(test.cmd); err != nil { + conn, err := internal.GetConnection("localhost", port) + if err != nil { t.Error(err) + return } - rv, _, err := r.ReadValue() + defer func() { + if conn != nil { + _ = conn.Close() + } + }() + + r := resp.NewConn(conn) + + tests := []struct { + name string + cmd []resp.Value + wantRes string + wantErr string + }{ + { + name: "1. Authenticate with default user without specifying username", + cmd: []resp.Value{resp.StringValue("AUTH"), resp.StringValue("password1")}, + wantRes: "OK", + wantErr: "", + }, + { + name: "2. Authenticate with plaintext password", + cmd: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("with_password_user"), + resp.StringValue("password2"), + }, + wantRes: "OK", + wantErr: "", + }, + { + name: "3. Authenticate with SHA256 password", + cmd: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("with_password_user"), + resp.StringValue("password3"), + }, + wantRes: "OK", + wantErr: "", + }, + { + name: "4. Authenticate with no password user", + cmd: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("no_password_user"), + resp.StringValue("password4"), + }, + wantRes: "OK", + wantErr: "", + }, + { + name: "5. Fail to authenticate with disabled user", + cmd: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("disabled_user"), + resp.StringValue("password5"), + }, + wantRes: "", + wantErr: "Error user disabled_user is disabled", + }, + { + name: "6. Fail to authenticate with non-existent user", + cmd: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("non_existent_user"), + resp.StringValue("password6"), + }, + wantRes: "", + wantErr: "Error no user with username non_existent_user", + }, + { + name: "7. Fail to authenticate with the wrong password", + cmd: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("with_password_user"), + resp.StringValue("wrong_password"), + }, + wantRes: "", + wantErr: "Error could not authenticate user", + }, + { + name: "8. Command too short", + cmd: []resp.Value{resp.StringValue("AUTH")}, + wantRes: "", + wantErr: fmt.Sprintf("Error %s", constants.WrongArgsResponse), + }, + { + name: "9. Command too long", + cmd: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("user"), + resp.StringValue("password1"), + resp.StringValue("password2"), + }, + wantRes: "", + wantErr: fmt.Sprintf("Error %s", constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err = r.WriteArray(test.cmd); err != nil { + t.Error(err) + } + rv, _, err := r.ReadValue() + if err != nil { + t.Error(err) + } + if test.wantErr != "" { + if rv.Error().Error() != test.wantErr { + t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, rv.Error().Error()) + } + return + } + if rv.String() != test.wantRes { + t.Errorf("expected response \"%s\", got \"%s\"", test.wantRes, rv.String()) + } + }) + } + }) + + t.Run("Test_Permissions", func(t *testing.T) { + port, err := internal.GetFreePort() if err != nil { t.Error(err) + return } - if test.wantErr != "" { - if rv.Error().Error() != test.wantErr { - t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, rv.Error().Error()) - } - continue + + mockServer, err := setUpServer(port, true, "") + if err != nil { + t.Error(err) + return } - if rv.String() != test.wantRes { - t.Errorf("expected response \"%s\", got \"%s\"", test.wantRes, rv.String()) + go func() { + mockServer.Start() + }() + + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return } - } -} + client := resp.NewConn(conn) -func Test_HandleCat(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) - if err != nil { - t.Error(err) - } - defer func() { - if conn != nil { + t.Cleanup(func() { _ = conn.Close() + mockServer.ShutDown() + }) + + // Add users to be used in test cases. + users := []echovault.User{ + { + // User with nokeys flag enables. + Username: "test_nokeys", + Enabled: true, + NoKeys: true, + AddPlainPasswords: []string{"test_nokeys_password"}, + }, + { + // This use will be used to test authorization failure when trying to access resources that are not + // in their "included" rules. + Username: "test_included", + Enabled: true, + AddPlainPasswords: []string{"test_included_password"}, + IncludeCategories: []string{ + constants.WriteCategory, + constants.ReadCategory, + constants.SlowCategory, + constants.PubSubCategory, + constants.ConnectionCategory, + constants.ListCategory, + }, + IncludeCommands: []string{"set", "get", "subscribe", "lrange", "ltrim"}, + IncludeChannels: []string{"channel[12]"}, + IncludeReadWriteKeys: []string{"key1", "key2"}, + }, + { + // This use will be used to test authorization failure when trying to access resources that are + // in their "excluded" rules. + Username: "test_excluded", + Enabled: true, + AddPlainPasswords: []string{"test_excluded_password"}, + IncludeCategories: []string{"*"}, + ExcludeCategories: []string{constants.FastCategory, constants.HashCategory}, + IncludeCommands: []string{"*"}, + ExcludeCommands: []string{"set", "mset"}, + IncludeChannels: []string{"*"}, + ExcludeChannels: []string{"channel[12]"}, + }, + } + for _, user := range users { + if _, err := mockServer.ACLSetUser(user); err != nil { + t.Error(err) + return + } } - }() - r := resp.NewConn(conn) - // Authenticate connection - if err = r.WriteArray([]resp.Value{resp.StringValue("AUTH"), resp.StringValue("password1")}); err != nil { - t.Error(err) - } - rv, _, err := r.ReadValue() - if err != nil { - t.Error(err) - } - if rv.String() != "OK" { - t.Error("could not authenticate user") - } + tests := []struct { + name string + auth []resp.Value + cmd []resp.Value + wantErr string + }{ + { + name: "1. Return error when the connection is not authenticated", + auth: []resp.Value{}, + cmd: []resp.Value{resp.StringValue("SET"), resp.StringValue("key"), resp.StringValue("value")}, + wantErr: "user must be authenticated", + }, + { + name: "2. Return error when the command category is not in the included categories section", + auth: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("test_included"), + resp.StringValue("test_included_password"), + }, + cmd: []resp.Value{ + resp.StringValue("HSET"), + resp.StringValue("hash"), + resp.StringValue("field1"), + resp.StringValue("value1"), + }, + wantErr: fmt.Sprintf("unauthorized access to the following categories: [@%s @%s]", + constants.FastCategory, constants.HashCategory), + }, + { + name: "3. Return error when the command category is in the excluded categories section", + auth: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("test_excluded"), + resp.StringValue("test_excluded_password"), + }, + cmd: []resp.Value{ + resp.StringValue("HSET"), + resp.StringValue("hash"), + resp.StringValue("field1"), + resp.StringValue("value1"), + }, + wantErr: fmt.Sprintf("unauthorized access to the following categories: [@%s]", + constants.HashCategory), + }, + { + name: "4. Return error when the command is not in the included command category", + auth: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("test_included"), + resp.StringValue("test_included_password"), + }, + cmd: []resp.Value{ + resp.StringValue("MSET"), + resp.StringValue("key1"), + resp.StringValue("value1"), + }, + wantErr: "not authorised to run MSET command", + }, + { + name: "5. Return error when the command is in the excluded command category", + auth: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("test_excluded"), + resp.StringValue("test_excluded_password"), + }, + cmd: []resp.Value{ + resp.StringValue("SET"), + resp.StringValue("key1"), + resp.StringValue("value1"), + }, + wantErr: "not authorised to run SET command", + }, + { + name: "6. Return error when subscribing to channel that's not in included channels", + auth: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("test_included"), + resp.StringValue("test_included_password"), + }, + cmd: []resp.Value{ + resp.StringValue("SUBSCRIBE"), + resp.StringValue("channel3"), + }, + wantErr: "not authorised to access channel &channel3", + }, + { + name: "7. Return error when publishing to channel that's in excluded channels", + auth: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("test_excluded"), + resp.StringValue("test_excluded_password"), + }, + cmd: []resp.Value{ + resp.StringValue("SUBSCRIBE"), + resp.StringValue("channel2"), + }, + wantErr: "not authorised to access channel &channel2", + }, + { + name: "8. Return error when the user has nokeys flag", + auth: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("test_nokeys"), + resp.StringValue("test_nokeys_password"), + }, + cmd: []resp.Value{resp.StringValue("GET"), resp.StringValue("key1")}, + }, + { + name: "9. Return error when trying to read from keys that are not in read keys list", + auth: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("test_included"), + resp.StringValue("test_included_password"), + }, + cmd: []resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue("key3"), + resp.StringValue("0"), + resp.StringValue("-1"), + }, + wantErr: fmt.Sprintf("not authorised to access the following keys: [%s~%s]", "%R", "key3"), + }, + { + name: "10. Return error when trying to write to keys that are not in write keys list", + auth: []resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue("test_included"), + resp.StringValue("test_included_password"), + }, + cmd: []resp.Value{ + resp.StringValue("LTRIM"), + resp.StringValue("key3"), + resp.StringValue("0"), + resp.StringValue("3"), + }, + wantErr: fmt.Sprintf("not authorised to access the following keys: [%s~%s]", "%W", "key3"), + }, + } - // Since only ACL commands are loaded in this test suite, this test will only test against the - // list of categories and commands available in the ACL module. - tests := []struct { - cmd []resp.Value - wantRes []string - wantErr string - }{ - { // 1. Return list of categories - cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT")}, - wantRes: []string{ - constants.ConnectionCategory, - constants.SlowCategory, - constants.FastCategory, - constants.AdminCategory, - constants.DangerousCategory, - }, - wantErr: "", - }, - { // 2. Return list of commands in connection category - cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue(constants.ConnectionCategory)}, - wantRes: []string{"auth"}, - wantErr: "", - }, - { // 3. Return list of commands in slow category - cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue(constants.SlowCategory)}, - wantRes: []string{"auth", "acl|cat", "acl|users", "acl|setuser", "acl|getuser", "acl|deluser", "acl|list", "acl|load", "acl|save"}, - wantErr: "", - }, - { // 4. Return list of commands in fast category - cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue(constants.FastCategory)}, - wantRes: []string{"acl|whoami"}, - wantErr: "", - }, - { // 5. Return list of commands in admin category - cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue(constants.AdminCategory)}, - wantRes: []string{"acl|users", "acl|setuser", "acl|getuser", "acl|deluser", "acl|list", "acl|load", "acl|save"}, - wantErr: "", - }, - { // 6. Return list of commands in dangerous category - cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue(constants.DangerousCategory)}, - wantRes: []string{"acl|users", "acl|setuser", "acl|getuser", "acl|deluser", "acl|list", "acl|load", "acl|save"}, - wantErr: "", - }, - { // 7. Return error when category does not exist - cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue("non-existent")}, - wantRes: nil, - wantErr: "Error category NON-EXISTENT not found", - }, - { // 8. Command too long - cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue("category1"), resp.StringValue("category2")}, - wantRes: nil, - wantErr: fmt.Sprintf("Error %s", constants.WrongArgsResponse), - }, - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Authenticate the user if the auth command is provided. + if len(test.auth) > 0 { + err := client.WriteArray(test.auth) + if err != nil { + t.Error(err) + return + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + return + } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected auth response to be OK, got \"%s\"", res.String()) + } + } - for _, test := range tests { - if err = r.WriteArray(test.cmd); err != nil { - t.Error(err) + if err := client.WriteArray(test.cmd); err != nil { + t.Error(err) + return + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + return + } + + if !strings.Contains(res.Error().Error(), test.wantErr) { + t.Errorf("expected error to contain string \"%s\", got \"%s\"", + test.wantErr, res.Error().Error()) + return + } + }) } - rv, _, err = r.ReadValue() + }) + + t.Run("Test_HandleCat", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) + return } - if test.wantErr != "" { - if rv.Error().Error() != test.wantErr { - t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, rv.Error().Error()) + defer func() { + if conn != nil { + _ = conn.Close() } - continue + }() + r := resp.NewConn(conn) + + // Authenticate connection + if err = r.WriteArray([]resp.Value{resp.StringValue("AUTH"), resp.StringValue("password1")}); err != nil { + t.Error(err) } - resArr := rv.Array() - // Check if all the elements in the expected array are in the response array - for _, expected := range test.wantRes { - if !slices.ContainsFunc(resArr, func(value resp.Value) bool { - return value.String() == expected - }) { - t.Errorf("could not find expected command \"%s\" in the response array for category", expected) - } + rv, _, err := r.ReadValue() + if err != nil { + t.Error(err) + } + if rv.String() != "OK" { + t.Error("could not authenticate user") } - } -} -func Test_HandleUsers(t *testing.T) { - port, _ := internal.GetFreePort() - mockServer := setUpServer(bindAddr, uint16(port), false, "") - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() - }() - wg.Wait() + // Since only ACL commands are loaded in this test suite, this test will only test against the + // list of categories and commands available in the ACL module. + tests := []struct { + cmd []resp.Value + wantRes []string + wantErr string + }{ + { // 1. Return list of categories + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT")}, + wantRes: []string{ + constants.ConnectionCategory, + constants.SlowCategory, + constants.FastCategory, + constants.AdminCategory, + constants.DangerousCategory, + }, + wantErr: "", + }, + { // 2. Return list of commands in connection category + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue(constants.ConnectionCategory)}, + wantRes: []string{"auth"}, + wantErr: "", + }, + { // 3. Return list of commands in slow category + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue(constants.SlowCategory)}, + wantRes: []string{"auth", "acl|cat", "acl|users", "acl|setuser", "acl|getuser", "acl|deluser", "acl|list", "acl|load", "acl|save"}, + wantErr: "", + }, + { // 4. Return list of commands in fast category + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue(constants.FastCategory)}, + wantRes: []string{"acl|whoami"}, + wantErr: "", + }, + { // 5. Return list of commands in admin category + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue(constants.AdminCategory)}, + wantRes: []string{"acl|users", "acl|setuser", "acl|getuser", "acl|deluser", "acl|list", "acl|load", "acl|save"}, + wantErr: "", + }, + { // 6. Return list of commands in dangerous category + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue(constants.DangerousCategory)}, + wantRes: []string{"acl|users", "acl|setuser", "acl|getuser", "acl|deluser", "acl|list", "acl|load", "acl|save"}, + wantErr: "", + }, + { // 7. Return error when category does not exist + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue("non-existent")}, + wantRes: nil, + wantErr: "Error category NON-EXISTENT not found", + }, + { // 8. Command too long + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("CAT"), resp.StringValue("category1"), resp.StringValue("category2")}, + wantRes: nil, + wantErr: fmt.Sprintf("Error %s", constants.WrongArgsResponse), + }, + } - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) - if err != nil { - t.Error(err) - } + for _, test := range tests { + if err = r.WriteArray(test.cmd); err != nil { + t.Error(err) + } + rv, _, err = r.ReadValue() + if err != nil { + t.Error(err) + } + if test.wantErr != "" { + if rv.Error().Error() != test.wantErr { + t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, rv.Error().Error()) + } + continue + } + resArr := rv.Array() + // Check if all the elements in the expected array are in the response array + for _, expected := range test.wantRes { + if !slices.ContainsFunc(resArr, func(value resp.Value) bool { + return value.String() == expected + }) { + t.Errorf("could not find expected command \"%s\" in the response array for category", expected) + } + } + } + }) - for { - // Wait until connection is not nil before continuing. - if conn != nil { - break + t.Run("Test_HandleUsers", func(t *testing.T) { + t.Parallel() + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return } - } - defer func() { - if conn != nil { - _ = conn.Close() + mockServer, err := setUpServer(port, false, "") + if err != nil { + t.Error(err) + return } - }() - r := resp.NewConn(conn) + go func() { + mockServer.Start() + }() - users := []string{"default", "with_password_user", "no_password_user", "disabled_user"} + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } - if err = r.WriteArray([]resp.Value{resp.StringValue("ACL"), resp.StringValue("USERS")}); err != nil { - t.Error(err) - } + defer func() { + if conn != nil { + _ = conn.Close() + } + }() - rv, _, err := r.ReadValue() - if err != nil { - t.Error(err) - } + r := resp.NewConn(conn) - resArr := rv.Array() + users := []string{"default", "with_password_user", "no_password_user", "disabled_user"} - // Check if all the expected users are in the response array - for _, user := range users { - if !slices.ContainsFunc(resArr, func(value resp.Value) bool { - return value.String() == user - }) { - t.Errorf("could not find expected user \"%s\" in response array", user) + if err = r.WriteArray([]resp.Value{resp.StringValue("ACL"), resp.StringValue("USERS")}); err != nil { + t.Error(err) } - } - // Check if all the users in the response array are in the expected users - for _, value := range resArr { - if !slices.ContainsFunc(users, func(user string) bool { - return value.String() == user - }) { - t.Errorf("could not find response user \"%s\" in expected users array", value.String()) + rv, _, err := r.ReadValue() + if err != nil { + t.Error(err) } - } -} -func Test_HandleSetUser(t *testing.T) { - port, _ := internal.GetFreePort() - mockServer := setUpServer(bindAddr, uint16(port), false, "") - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() - }() - wg.Wait() + resArr := rv.Array() - a := getACL(mockServer) + // Check if all the expected users are in the response array + for _, user := range users { + if !slices.ContainsFunc(resArr, func(value resp.Value) bool { + return value.String() == user + }) { + t.Errorf("could not find expected user \"%s\" in response array", user) + } + } - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) - if err != nil { - t.Error(err) - } - defer func() { - if conn != nil { - _ = conn.Close() + // Check if all the users in the response array are in the expected users + for _, value := range resArr { + if !slices.ContainsFunc(users, func(user string) bool { + return value.String() == user + }) { + t.Errorf("could not find response user \"%s\" in expected users array", value.String()) + } } - }() + }) - r := resp.NewConn(conn) + t.Run("Test_HandleSetUser", func(t *testing.T) { + t.Parallel() + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } - tests := []struct { - presetUser *acl.User - cmd []resp.Value - wantRes string - wantErr string - wantUser *acl.User - }{ - { - // 1. Create new enabled user - presetUser: nil, - cmd: []resp.Value{ - resp.StringValue("ACL"), - resp.StringValue("SETUSER"), - resp.StringValue("set_user_1"), - resp.StringValue("on"), - }, - wantRes: "OK", - wantErr: "", - wantUser: func() *acl.User { - user := acl.CreateUser("set_user_1") - user.Enabled = true - user.Normalise() - return user - }(), - }, - { - // 2. Create new disabled user - presetUser: nil, - cmd: []resp.Value{ - resp.StringValue("ACL"), - resp.StringValue("SETUSER"), - resp.StringValue("set_user_2"), - resp.StringValue("off"), - }, - wantRes: "OK", - wantErr: "", - wantUser: func() *acl.User { - user := acl.CreateUser("set_user_2") - user.Enabled = false - user.Normalise() - return user - }(), - }, - { - // 3. Create new enabled user with both plaintext and SHA256 passwords - presetUser: nil, - cmd: []resp.Value{ - resp.StringValue("ACL"), - resp.StringValue("SETUSER"), - resp.StringValue("set_user_3"), - resp.StringValue("on"), - resp.StringValue(">set_user_3_plaintext_password_1"), - resp.StringValue(">set_user_3_plaintext_password_2"), - resp.StringValue(fmt.Sprintf("#%s", generateSHA256Password("set_user_3_hash_password_1"))), - resp.StringValue(fmt.Sprintf("#%s", generateSHA256Password("set_user_3_hash_password_2"))), - }, - wantRes: "OK", - wantErr: "", - wantUser: func() *acl.User { - user := acl.CreateUser("set_user_3") - user.Enabled = true - user.Passwords = []acl.Password{ - {PasswordType: acl.PasswordPlainText, PasswordValue: "set_user_3_plaintext_password_1"}, - {PasswordType: acl.PasswordPlainText, PasswordValue: "set_user_3_plaintext_password_2"}, - {PasswordType: acl.PasswordSHA256, PasswordValue: generateSHA256Password("set_user_3_hash_password_1")}, - {PasswordType: acl.PasswordSHA256, PasswordValue: generateSHA256Password("set_user_3_hash_password_2")}, - } - user.Normalise() - return user - }(), - }, - { - // 4. Remove plaintext and SHA256 password from existing user - presetUser: func() *acl.User { - user := acl.CreateUser("set_user_4") - user.Enabled = true - user.Passwords = []acl.Password{ - {PasswordType: acl.PasswordPlainText, PasswordValue: "set_user_3_plaintext_password_1"}, - {PasswordType: acl.PasswordPlainText, PasswordValue: "set_user_3_plaintext_password_2"}, - {PasswordType: acl.PasswordSHA256, PasswordValue: generateSHA256Password("set_user_3_hash_password_1")}, - {PasswordType: acl.PasswordSHA256, PasswordValue: generateSHA256Password("set_user_3_hash_password_2")}, - } - user.Normalise() - return user - }(), - cmd: []resp.Value{ - resp.StringValue("ACL"), - resp.StringValue("SETUSER"), - resp.StringValue("set_user_4"), - resp.StringValue("on"), - resp.StringValue("password1"), - resp.StringValue(fmt.Sprintf("#%s", generateSHA256Password("password2"))), - }, - wantRes: "OK", - wantErr: "", - wantUser: func() *acl.User { - user := acl.CreateUser("set_user_16") - user.Enabled = true - user.NoPassword = true - user.Passwords = []acl.Password{} - user.Normalise() - return user - }(), - }, - { - // 17. Delete all existing users passwords using 'nopass' - presetUser: func() *acl.User { - user := acl.CreateUser("set_user_17") - user.Enabled = true - user.NoPassword = true - user.Passwords = []acl.Password{ - {PasswordType: acl.PasswordPlainText, PasswordValue: "password1"}, - {PasswordType: acl.PasswordSHA256, PasswordValue: generateSHA256Password("password2")}, - } - user.Normalise() - return user - }(), - cmd: []resp.Value{ - resp.StringValue("ACL"), - resp.StringValue("SETUSER"), - resp.StringValue("set_user_17"), - resp.StringValue("on"), - resp.StringValue("nopass"), - }, - wantRes: "OK", - wantErr: "", - wantUser: func() *acl.User { - user := acl.CreateUser("set_user_17") - user.Enabled = true - user.NoPassword = true - user.Passwords = []acl.Password{} - user.Normalise() - return user - }(), - }, - { - // 18. Clear all of an existing user's passwords using 'resetpass' - presetUser: func() *acl.User { - user := acl.CreateUser("set_user_18") - user.Enabled = true - user.NoPassword = true - user.Passwords = []acl.Password{ - {PasswordType: acl.PasswordPlainText, PasswordValue: "password1"}, - {PasswordType: acl.PasswordSHA256, PasswordValue: generateSHA256Password("password2")}, - } - user.Normalise() - return user - }(), - cmd: []resp.Value{ - resp.StringValue("ACL"), - resp.StringValue("SETUSER"), - resp.StringValue("set_user_18"), - resp.StringValue("on"), - resp.StringValue("nopass"), - }, - wantRes: "OK", - wantErr: "", - wantUser: func() *acl.User { - user := acl.CreateUser("set_user_18") - user.Enabled = true - user.NoPassword = true - user.Passwords = []acl.Password{} - user.Normalise() - return user - }(), - }, - { - // 19. Clear all of an existing user's command privileges using 'nocommands' - presetUser: func() *acl.User { - user := acl.CreateUser("set_user_19") - user.Enabled = true - user.IncludedCommands = []string{"acl|getuser", "acl|setuser", "acl|deluser"} - user.ExcludedCommands = []string{"rewriteaof", "save"} - user.Normalise() - return user - }(), - cmd: []resp.Value{ - resp.StringValue("ACL"), - resp.StringValue("SETUSER"), - resp.StringValue("set_user_19"), - resp.StringValue("on"), - resp.StringValue("nocommands"), - }, - wantRes: "OK", - wantErr: "", - wantUser: func() *acl.User { - user := acl.CreateUser("set_user_19") - user.Enabled = true - user.IncludedCommands = []string{} - user.ExcludedCommands = []string{"*"} - user.IncludedCategories = []string{} - user.ExcludedCategories = []string{"*"} - user.Normalise() - return user - }(), - }, - { - // 20. Clear all of an existing user's allowed keys using 'resetkeys' - presetUser: func() *acl.User { - user := acl.CreateUser("set_user_20") - user.Enabled = true - user.IncludedWriteKeys = []string{"key1", "key2", "key3", "key4", "key5", "key6"} - user.IncludedReadKeys = []string{"key1", "key2", "key3", "key7", "key8", "key9"} - user.Normalise() - return user - }(), - cmd: []resp.Value{ - resp.StringValue("ACL"), - resp.StringValue("SETUSER"), - resp.StringValue("set_user_20"), - resp.StringValue("on"), - resp.StringValue("resetkeys"), - }, - wantRes: "OK", - wantErr: "", - wantUser: func() *acl.User { - user := acl.CreateUser("set_user_20") - user.Enabled = true - user.NoKeys = true - user.IncludedReadKeys = []string{} - user.IncludedWriteKeys = []string{} - user.Normalise() - return user - }(), - }, - { - // 21. Allow user to access all channels using 'resetchannels' - presetUser: func() *acl.User { - user := acl.CreateUser("set_user_21") - user.IncludedPubSubChannels = []string{"channel1", "channel2"} - user.ExcludedPubSubChannels = []string{"channel3", "channel4"} - user.Normalise() - return user - }(), - cmd: []resp.Value{ - resp.StringValue("ACL"), - resp.StringValue("SETUSER"), - resp.StringValue("set_user_21"), - resp.StringValue("resetchannels"), - }, - wantRes: "OK", - wantErr: "", - wantUser: func() *acl.User { - user := acl.CreateUser("set_user_21") - user.IncludedPubSubChannels = []string{} - user.ExcludedPubSubChannels = []string{"*"} - user.Normalise() - return user - }(), - }, - } - - for i, test := range tests { - if test.presetUser != nil { - a.AddUsers([]*acl.User{test.presetUser}) - } - if err = r.WriteArray(test.cmd); err != nil { + mockServer, err := setUpServer(port, false, "") + if err != nil { t.Error(err) + return } - v, _, err := r.ReadValue() + + go func() { + mockServer.Start() + }() + + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) + return } - if test.wantErr != "" { - if v.Error().Error() != test.wantErr { - t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, v.Error().Error()) + defer func() { + if conn != nil { + _ = conn.Close() } - continue - } - if v.String() != test.wantRes { - t.Errorf("expected response \"%s\", got \"%s\"", test.wantRes, v.String()) - } - if test.wantUser == nil { - continue - } - expectedUser := test.wantUser - currUserIdx := slices.IndexFunc(a.Users, func(user *acl.User) bool { - return user.Username == expectedUser.Username - }) - if currUserIdx == -1 { - t.Errorf("expected to find user with username \"%s\" but could not find them.", expectedUser.Username) - } - if err = compareUsers(expectedUser, a.Users[currUserIdx]); err != nil { - t.Errorf("test idx: %d, %+v", i, err) - } - } -} + }() -func Test_HandleGetUser(t *testing.T) { - port, _ := internal.GetFreePort() - mockServer := setUpServer(bindAddr, uint16(port), false, "") - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() - }() - wg.Wait() - - a := getACL(mockServer) + r := resp.NewConn(conn) - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) - if err != nil { - t.Error(err) - } - defer func() { - if conn != nil { - _ = conn.Close() - } - }() + t.Cleanup(func() { + mockServer.ShutDown() + }) - r := resp.NewConn(conn) - - tests := []struct { - presetUser *acl.User - cmd []resp.Value - wantRes []resp.Value - wantErr string - }{ - { // 1. Get the user and all their details - presetUser: &acl.User{ - Username: "get_user_1", - Enabled: true, - NoPassword: false, - NoKeys: false, - Passwords: []acl.Password{ - {PasswordType: acl.PasswordPlainText, PasswordValue: "get_user_password_1"}, - {PasswordType: acl.PasswordSHA256, PasswordValue: generateSHA256Password("get_user_password_2")}, - }, - IncludedCategories: []string{constants.WriteCategory, constants.ReadCategory, constants.PubSubCategory}, - ExcludedCategories: []string{constants.AdminCategory, constants.ConnectionCategory, constants.DangerousCategory}, - IncludedCommands: []string{"acl|setuser", "acl|getuser", "acl|deluser"}, - ExcludedCommands: []string{"rewriteaof", "save", "acl|load", "acl|save"}, - IncludedReadKeys: []string{"key1", "key2", "key3", "key4"}, - IncludedWriteKeys: []string{"key1", "key2", "key5", "key6"}, - IncludedPubSubChannels: []string{"channel1", "channel2"}, - ExcludedPubSubChannels: []string{"channel3", "channel4"}, - }, - cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("GETUSER"), resp.StringValue("get_user_1")}, - wantRes: []resp.Value{ - resp.StringValue("username"), - resp.ArrayValue([]resp.Value{resp.StringValue("get_user_1")}), - resp.StringValue("flags"), - resp.ArrayValue([]resp.Value{ + tests := []struct { + name string + presetUser *echovault.User + cmd []resp.Value + wantRes string + wantErr string + wantUser map[string][]string + }{ + { + name: "1. Create new enabled user", + presetUser: nil, + cmd: []resp.Value{ + resp.StringValue("ACL"), + resp.StringValue("SETUSER"), + resp.StringValue("set_user_1"), + resp.StringValue("on"), + }, + wantRes: "OK", + wantErr: "", + wantUser: map[string][]string{ + "username": {"set_user_1"}, + "flags": {"on"}, + "categories": {"+@all"}, + "commands": {"+all"}, + "keys": {"%RW~*"}, + "channels": {"+&*"}, + }, + }, + { + name: "2. Create new disabled user", + presetUser: nil, + cmd: []resp.Value{ + resp.StringValue("ACL"), + resp.StringValue("SETUSER"), + resp.StringValue("set_user_2"), + resp.StringValue("off"), + }, + wantRes: "OK", + wantErr: "", + wantUser: map[string][]string{ + "username": {"set_user_2"}, + "flags": {"off"}, + "categories": {"+@all"}, + "commands": {"+all"}, + "keys": {"%RW~*"}, + "channels": {"+&*"}, + }, + }, + { + name: "3. Create new enabled user with both plaintext and SHA256 passwords", + presetUser: nil, + cmd: []resp.Value{ + resp.StringValue("ACL"), + resp.StringValue("SETUSER"), + resp.StringValue("set_user_3"), + resp.StringValue("on"), + resp.StringValue(">set_user_3_plaintext_password_1"), + resp.StringValue(">set_user_3_plaintext_password_2"), + resp.StringValue(fmt.Sprintf("#%s", generateSHA256Password("set_user_3_hash_password_1"))), + resp.StringValue(fmt.Sprintf("#%s", generateSHA256Password("set_user_3_hash_password_2"))), + }, + wantRes: "OK", + wantErr: "", + wantUser: map[string][]string{ + "username": {"set_user_3"}, + "flags": {"on"}, + "categories": {"+@all"}, + "commands": {"+all"}, + "keys": {"%RW~*"}, + "channels": {"+&*"}, + }, + }, + { + name: "4. Remove plaintext and SHA256 password from existing user", + presetUser: &echovault.User{ + Username: "set_user_4", + Enabled: true, + AddPlainPasswords: []string{"set_user_4_plaintext_password_1", "set_user_4_plaintext_password_2"}, + AddHashPasswords: []string{ + generateSHA256Password("set_user_4_hash_password_1"), + generateSHA256Password("set_user_4_hash_password_2"), + }, + }, + cmd: []resp.Value{ + resp.StringValue("ACL"), + resp.StringValue("SETUSER"), + resp.StringValue("set_user_4"), + resp.StringValue("on"), + resp.StringValue("password1"), + resp.StringValue(fmt.Sprintf("#%s", generateSHA256Password("password2"))), + }, + wantRes: "OK", + wantErr: "", + wantUser: map[string][]string{ + "username": {"set_user_16"}, + "flags": {"on", "nopass"}, + "categories": {"+@all"}, + "commands": {"+all"}, + "keys": {"%RW~*"}, + "channels": {"+&*"}, + }, + }, + { + name: "17. Delete all existing users passwords using 'nopass'", + presetUser: &echovault.User{ + Username: "set_user_17", + Enabled: true, + NoPassword: true, + AddPlainPasswords: []string{"password1"}, + AddHashPasswords: []string{generateSHA256Password("password2")}, + }, + cmd: []resp.Value{ + resp.StringValue("ACL"), + resp.StringValue("SETUSER"), + resp.StringValue("set_user_17"), + resp.StringValue("on"), + resp.StringValue("nopass"), + }, + wantRes: "OK", + wantErr: "", + wantUser: map[string][]string{ + "username": {"set_user_17"}, + "flags": {"on", "nopass"}, + "categories": {"+@all"}, + "commands": {"+all"}, + "keys": {"%RW~*"}, + "channels": {"+&*"}, + }, + }, + { + name: "18. Clear all of an existing user's passwords using 'resetpass'", + presetUser: &echovault.User{ + Username: "set_user_18", + Enabled: true, + NoPassword: true, + AddPlainPasswords: []string{"password1"}, + AddHashPasswords: []string{generateSHA256Password("password2")}, + }, + cmd: []resp.Value{ + resp.StringValue("ACL"), + resp.StringValue("SETUSER"), + resp.StringValue("set_user_18"), + resp.StringValue("on"), + resp.StringValue("nopass"), + }, + wantRes: "OK", + wantErr: "", + wantUser: map[string][]string{ + "username": {"set_user_18"}, + "flags": {"on", "nopass"}, + "categories": {"+@all"}, + "commands": {"+all"}, + "keys": {"%RW~*"}, + "channels": {"+&*"}, + }, + }, + { + name: "19. Clear all of an existing user's command privileges using 'nocommands'", + presetUser: &echovault.User{ + Username: "set_user_19", + Enabled: true, + IncludeCommands: []string{"acl|getuser", "acl|setuser", "acl|deluser"}, + ExcludeCommands: []string{"rewriteaof", "save"}, + }, + cmd: []resp.Value{ + resp.StringValue("ACL"), + resp.StringValue("SETUSER"), + resp.StringValue("set_user_19"), + resp.StringValue("on"), + resp.StringValue("nocommands"), + }, + wantRes: "OK", + wantErr: "", + wantUser: map[string][]string{ + "username": {"set_user_19"}, + "flags": {"on"}, + "categories": {"-@all"}, + "commands": {"-all"}, + "keys": {"%RW~*"}, + "channels": {"+&*"}, + }, + }, + { + name: "20. Clear all of an existing user's allowed keys using 'resetkeys'", + presetUser: &echovault.User{ + Username: "set_user_20", + Enabled: true, + IncludeWriteKeys: []string{"key1", "key2", "key3", "key4", "key5", "key6"}, + IncludeReadKeys: []string{"key1", "key2", "key3", "key7", "key8", "key9"}, + }, + cmd: []resp.Value{ + resp.StringValue("ACL"), + resp.StringValue("SETUSER"), + resp.StringValue("set_user_20"), + resp.StringValue("on"), + resp.StringValue("resetkeys"), + }, + wantRes: "OK", + wantErr: "", + wantUser: map[string][]string{ + "username": {"set_user_20"}, + "flags": {"on", "nokeys"}, + "categories": {"+@all"}, + "commands": {"+all"}, + "keys": {}, + "channels": {"+&*"}, + }, + }, + { + name: "21. Allow user to access all channels using 'resetchannels'", + presetUser: &echovault.User{ + Username: "set_user_21", + Enabled: true, + IncludeChannels: []string{"channel1", "channel2"}, + ExcludeChannels: []string{"channel3", "channel4"}, + }, + cmd: []resp.Value{ + resp.StringValue("ACL"), + resp.StringValue("SETUSER"), + resp.StringValue("set_user_21"), + resp.StringValue("resetchannels"), + }, + wantRes: "OK", + wantErr: "", + wantUser: map[string][]string{ + "username": {"set_user_21"}, + "flags": {"on"}, + "categories": {"+@all"}, + "commands": {"+all"}, + "keys": {"%RW~*"}, + "channels": {"-&*"}, + }, + }, + } - for _, test := range tests { - if test.presetUser != nil { - a.AddUsers([]*acl.User{test.presetUser}) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetUser != nil { + if _, err := mockServer.ACLSetUser(*test.presetUser); err != nil { + t.Error(err) + return + } + } + if err = r.WriteArray(test.cmd); err != nil { + t.Error(err) + } + v, _, err := r.ReadValue() + if err != nil { + t.Error(err) + } + if test.wantErr != "" { + if v.Error().Error() != test.wantErr { + t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, v.Error().Error()) + } + return + } + if v.String() != test.wantRes { + t.Errorf("expected response \"%s\", got \"%s\"", test.wantRes, v.String()) + } + if test.wantUser == nil { + return + } + + user, err := mockServer.ACLGetUser(test.wantUser["username"][0]) + if err != nil { + t.Error(err) + return + } + + if err = compareUsers(test.wantUser, user); err != nil { + t.Error(err) + return + } + }) } - if err = r.WriteArray(test.cmd); err != nil { + }) + + t.Run("Test_HandleGetUser", func(t *testing.T) { + t.Parallel() + port, err := internal.GetFreePort() + if err != nil { t.Error(err) + return } - v, _, err := r.ReadValue() + + mockServer, err := setUpServer(port, false, "") if err != nil { t.Error(err) + return } - if test.wantErr != "" { - if v.Error().Error() != test.wantErr { - t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, v.Error().Error()) + go func() { + mockServer.Start() + }() + + t.Cleanup(func() { + mockServer.ShutDown() + }) + + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + if conn != nil { + _ = conn.Close() } - continue + }() + + r := resp.NewConn(conn) + + tests := []struct { + name string + presetUser *echovault.User + cmd []resp.Value + wantRes []resp.Value + wantErr string + }{ + { + name: "1. Get the user and all their details", + presetUser: &echovault.User{ + Username: "get_user_1", + Enabled: true, + NoPassword: false, + NoKeys: false, + AddPlainPasswords: []string{"get_user_password_1"}, + AddHashPasswords: []string{generateSHA256Password("get_user_password_2")}, + IncludeCategories: []string{constants.WriteCategory, constants.ReadCategory, constants.PubSubCategory}, + ExcludeCategories: []string{constants.AdminCategory, constants.ConnectionCategory, constants.DangerousCategory}, + IncludeCommands: []string{"acl|setuser", "acl|getuser", "acl|deluser"}, + ExcludeCommands: []string{"rewriteaof", "save", "acl|load", "acl|save"}, + IncludeReadKeys: []string{"key1", "key2", "key3", "key4"}, + IncludeWriteKeys: []string{"key1", "key2", "key5", "key6"}, + IncludeChannels: []string{"channel1", "channel2"}, + ExcludeChannels: []string{"channel3", "channel4"}, + }, + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("GETUSER"), resp.StringValue("get_user_1")}, + wantRes: []resp.Value{ + resp.StringValue("username"), + resp.ArrayValue([]resp.Value{resp.StringValue("get_user_1")}), + resp.StringValue("flags"), + resp.ArrayValue([]resp.Value{ + resp.StringValue("on"), + }), + resp.StringValue("categories"), + resp.ArrayValue([]resp.Value{ + resp.StringValue(fmt.Sprintf("+@%s", constants.WriteCategory)), + resp.StringValue(fmt.Sprintf("+@%s", constants.ReadCategory)), + resp.StringValue(fmt.Sprintf("+@%s", constants.PubSubCategory)), + resp.StringValue(fmt.Sprintf("-@%s", constants.AdminCategory)), + resp.StringValue(fmt.Sprintf("-@%s", constants.ConnectionCategory)), + resp.StringValue(fmt.Sprintf("-@%s", constants.DangerousCategory)), + }), + resp.StringValue("commands"), + resp.ArrayValue([]resp.Value{ + resp.StringValue("+acl|setuser"), + resp.StringValue("+acl|getuser"), + resp.StringValue("+acl|deluser"), + resp.StringValue("-rewriteaof"), + resp.StringValue("-save"), + resp.StringValue("-acl|load"), + resp.StringValue("-acl|save"), + }), + resp.StringValue("keys"), + resp.ArrayValue([]resp.Value{ + // Keys here + resp.StringValue("%RW~key1"), + resp.StringValue("%RW~key2"), + resp.StringValue("%R~key3"), + resp.StringValue("%R~key4"), + resp.StringValue("%W~key5"), + resp.StringValue("%W~key6"), + }), + resp.StringValue("channels"), + resp.ArrayValue([]resp.Value{ + // Channels here + resp.StringValue("+&channel1"), + resp.StringValue("+&channel2"), + resp.StringValue("-&channel3"), + resp.StringValue("-&channel4"), + }), + }, + wantErr: "", + }, + { + name: "2. Return user not found error", + presetUser: nil, + cmd: []resp.Value{ + resp.StringValue("ACL"), + resp.StringValue("GETUSER"), + resp.StringValue("non_existent_user")}, + wantRes: nil, + wantErr: "Error user not found", + }, } - resArr := v.Array() - for i := 0; i < len(resArr); i++ { - if slices.Contains([]string{"username", "flags", "categories", "commands", "keys", "channels"}, resArr[i].String()) { - // String item - if resArr[i].String() != test.wantRes[i].String() { - t.Errorf("expected response component %+v, got %+v", test.wantRes[i], resArr[i]) - } - } else { - // Array item - var expected []string - for _, item := range test.wantRes[i].Array() { - expected = append(expected, item.String()) - } - var res []string - for _, item := range resArr[i].Array() { - res = append(res, item.String()) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetUser != nil { + if _, err := mockServer.ACLSetUser(*test.presetUser); err != nil { + t.Error(err) + return + } } - - if err = compareSlices(res, expected); err != nil { + if err = r.WriteArray(test.cmd); err != nil { t.Error(err) } - } + v, _, err := r.ReadValue() + if err != nil { + t.Error(err) + } + if test.wantErr != "" { + if v.Error().Error() != test.wantErr { + t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, v.Error().Error()) + } + return + } + resArr := v.Array() + for i := 0; i < len(resArr); i++ { + if slices.Contains([]string{"username", "flags", "categories", "commands", "keys", "channels"}, resArr[i].String()) { + // String item + if resArr[i].String() != test.wantRes[i].String() { + t.Errorf("expected response component %+v, got %+v", test.wantRes[i], resArr[i]) + } + } else { + // Array item + var expected []string + for _, item := range test.wantRes[i].Array() { + expected = append(expected, item.String()) + } + + var res []string + for _, item := range resArr[i].Array() { + res = append(res, item.String()) + } + + if err = compareSlices(res, expected); err != nil { + t.Error(err) + } + } + } + }) } - } -} - -func Test_HandleDelUser(t *testing.T) { - port, _ := internal.GetFreePort() - mockServer := setUpServer(bindAddr, uint16(port), false, "") - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() - }() - wg.Wait() + }) - a := getACL(mockServer) + t.Run("Test_HandleDelUser", func(t *testing.T) { + t.Parallel() + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) - if err != nil { - t.Error(err) - } - defer func() { - if conn != nil { - _ = conn.Close() + mockServer, err := setUpServer(port, false, "") + if err != nil { + t.Error(err) + return } - }() - r := resp.NewConn(conn) + go func() { + mockServer.Start() + }() - tests := []struct { - presetUser *acl.User - cmd []resp.Value - wantRes string - wantErr string - }{ - { - // 1. Delete existing user while skipping default user and non-existent user - presetUser: acl.CreateUser("user_to_delete"), - cmd: []resp.Value{ - resp.StringValue("ACL"), - resp.StringValue("DELUSER"), - resp.StringValue("default"), - resp.StringValue("user_to_delete"), - resp.StringValue("non_existent_user"), - }, - wantRes: "OK", - wantErr: "", - }, - { - // 2. Command too short - presetUser: nil, - cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("DELUSER")}, - wantRes: "", - wantErr: fmt.Sprintf("Error %s", constants.WrongArgsResponse), - }, - } + t.Cleanup(func() { + mockServer.ShutDown() + }) - for _, test := range tests { - if test.presetUser != nil { - a.AddUsers([]*acl.User{test.presetUser}) - } - if err = r.WriteArray(test.cmd); err != nil { - t.Error(err) - } - v, _, err := r.ReadValue() + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) + return } - if test.wantErr != "" { - if v.Error().Error() != test.wantErr { - t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, v.Error().Error()) + defer func() { + if conn != nil { + _ = conn.Close() } - continue - } - // Check that default user still exists in the list of users - if !slices.ContainsFunc(a.Users, func(user *acl.User) bool { - return user.Username == "default" - }) { - t.Error("could not find user with username \"default\" in the ACL after deleting user") - } - // Check that the deleted user is no longer in the list - if slices.ContainsFunc(a.Users, func(user *acl.User) bool { - return user.Username == "user_to_delete" - }) { - t.Error("deleted user found in the ACL") + }() + r := resp.NewConn(conn) + + tests := []struct { + name string + presetUser *echovault.User + cmd []resp.Value + wantRes string + wantErr string + }{ + { + name: "1. Delete existing user while skipping default user and non-existent user", + presetUser: &echovault.User{ + Username: "user_to_delete", + Enabled: true, + }, + cmd: []resp.Value{ + resp.StringValue("ACL"), + resp.StringValue("DELUSER"), + resp.StringValue("default"), + resp.StringValue("user_to_delete"), + resp.StringValue("non_existent_user"), + }, + wantRes: "OK", + wantErr: "", + }, + { + name: "2. Command too short", + presetUser: nil, + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("DELUSER")}, + wantRes: "", + wantErr: fmt.Sprintf("Error %s", constants.WrongArgsResponse), + }, } - } -} -func Test_HandleWhoAmI(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) - if err != nil { - t.Error(err) - } - defer func() { - if conn != nil { - _ = conn.Close() - } - }() + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetUser != nil { + if _, err := mockServer.ACLSetUser(*test.presetUser); err != nil { + t.Error(err) + return + } + } + if err = r.WriteArray(test.cmd); err != nil { + t.Error(err) + } + v, _, err := r.ReadValue() + if err != nil { + t.Error(err) + } + if test.wantErr != "" { + if v.Error().Error() != test.wantErr { + t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, v.Error().Error()) + } + return + } - r := resp.NewConn(conn) - - tests := []struct { - username string - password string - wantRes string - }{ - { // 1. With default user - username: "default", - password: "password1", - wantRes: "default", - }, - { // 2. With user authenticated by plaintext password - username: "with_password_user", - password: "password2", - wantRes: "with_password_user", - }, - { // 3. With user authenticated by SHA256 password - username: "with_password_user", - password: "password3", - wantRes: "with_password_user", - }, - } + usernames, err := mockServer.ACLUsers() + if err != nil { + t.Error(err) + return + } - for _, test := range tests { - // Authenticate - if err = r.WriteArray([]resp.Value{ - resp.StringValue("AUTH"), - resp.StringValue(test.username), - resp.StringValue(test.password), - }); err != nil { - t.Error(err) + // Check that default user still exists in the list of users + if !slices.Contains(usernames, "default") { + t.Error("could not find user with username \"default\" in the ACL after deleting user") + return + } + + // Check that the deleted user is no longer in the list + if slices.Contains(usernames, "user_to_delete") { + t.Error("deleted user found in the ACL") + return + } + }) } - v, _, err := r.ReadValue() + }) + + t.Run("Test_HandleWhoAmI", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) + return } - if v.String() != "OK" { - t.Errorf("expected response for auth with %s:%s to be \"OK\", got %s", test.username, test.password, v.String()) + defer func() { + if conn != nil { + _ = conn.Close() + } + }() + + r := resp.NewConn(conn) + + tests := []struct { + name string + username string + password string + wantRes string + }{ + { + name: "1. With default user", + username: "default", + password: "password1", + wantRes: "default", + }, + { + name: "2. With user authenticated by plaintext password", + username: "with_password_user", + password: "password2", + wantRes: "with_password_user", + }, + { + name: "3. With user authenticated by SHA256 password", + username: "with_password_user", + password: "password3", + wantRes: "with_password_user", + }, } - // Check whoami response value - if err = r.WriteArray([]resp.Value{resp.StringValue("ACL"), resp.StringValue("WHOAMI")}); err != nil { - t.Error(err) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Authenticate + if err = r.WriteArray([]resp.Value{ + resp.StringValue("AUTH"), + resp.StringValue(test.username), + resp.StringValue(test.password), + }); err != nil { + t.Error(err) + } + v, _, err := r.ReadValue() + if err != nil { + t.Error(err) + } + if v.String() != "OK" { + t.Errorf("expected response for auth with %s:%s to be \"OK\", got %s", test.username, test.password, v.String()) + } + // Check whoami response value + if err = r.WriteArray([]resp.Value{resp.StringValue("ACL"), resp.StringValue("WHOAMI")}); err != nil { + t.Error(err) + } + v, _, err = r.ReadValue() + if err != nil { + t.Error(err) + } + if v.String() != test.wantRes { + t.Errorf("expected whoami response to be \"%s\", got \"%s\"", test.wantRes, v.String()) + } + }) } - v, _, err = r.ReadValue() + }) + + t.Run("Test_HandleList", func(t *testing.T) { + t.Parallel() + port, err := internal.GetFreePort() if err != nil { t.Error(err) + return } - if v.String() != test.wantRes { - t.Errorf("expected whoami response to be \"%s\", got \"%s\"", test.wantRes, v.String()) - } - } -} -func Test_HandleList(t *testing.T) { - port, _ := internal.GetFreePort() - mockServer := setUpServer(bindAddr, uint16(port), false, "") - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() - }() - wg.Wait() + mockServer, err := setUpServer(port, false, "") + if err != nil { + t.Error(err) + return + } + go func() { + mockServer.Start() + }() - a := getACL(mockServer) + t.Cleanup(func() { + mockServer.ShutDown() + }) - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) - if err != nil { - t.Error(err) - } - defer func() { - if conn != nil { - _ = conn.Close() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return } - }() - - r := resp.NewConn(conn) - - tests := []struct { - presetUsers []*acl.User - cmd []resp.Value - wantRes []string - wantErr string - }{ - { // 1. Get the user and all their details - presetUsers: []*acl.User{ - { - Username: "list_user_1", - Enabled: true, - NoPassword: false, - NoKeys: false, - Passwords: []acl.Password{ - {PasswordType: acl.PasswordPlainText, PasswordValue: "list_user_password_1"}, - {PasswordType: acl.PasswordSHA256, PasswordValue: generateSHA256Password("list_user_password_2")}, + defer func() { + if conn != nil { + _ = conn.Close() + } + }() + + r := resp.NewConn(conn) + + tests := []struct { + name string + presetUsers []*echovault.User + cmd []resp.Value + wantRes []string + wantErr string + }{ + { + name: "1. Get the user and all their details", + presetUsers: []*echovault.User{ + { + Username: "list_user_1", + Enabled: true, + NoPassword: false, + NoKeys: false, + AddPlainPasswords: []string{"list_user_password_1"}, + AddHashPasswords: []string{generateSHA256Password("list_user_password_2")}, + IncludeCategories: []string{constants.WriteCategory, constants.ReadCategory, constants.PubSubCategory}, + ExcludeCategories: []string{constants.AdminCategory, constants.ConnectionCategory, constants.DangerousCategory}, + IncludeCommands: []string{"acl|setuser", "acl|getuser", "acl|deluser"}, + ExcludeCommands: []string{"rewriteaof", "save", "acl|load", "acl|save"}, + IncludeReadKeys: []string{"key1", "key2", "key3", "key4"}, + IncludeWriteKeys: []string{"key1", "key2", "key5", "key6"}, + IncludeChannels: []string{"channel1", "channel2"}, + ExcludeChannels: []string{"channel3", "channel4"}, }, - IncludedCategories: []string{constants.WriteCategory, constants.ReadCategory, constants.PubSubCategory}, - ExcludedCategories: []string{constants.AdminCategory, constants.ConnectionCategory, constants.DangerousCategory}, - IncludedCommands: []string{"acl|setuser", "acl|getuser", "acl|deluser"}, - ExcludedCommands: []string{"rewriteaof", "save", "acl|load", "acl|save"}, - IncludedReadKeys: []string{"key1", "key2", "key3", "key4"}, - IncludedWriteKeys: []string{"key1", "key2", "key5", "key6"}, - IncludedPubSubChannels: []string{"channel1", "channel2"}, - ExcludedPubSubChannels: []string{"channel3", "channel4"}, - }, - { - Username: "list_user_2", - Enabled: true, - NoPassword: true, - NoKeys: true, - Passwords: []acl.Password{}, - IncludedCategories: []string{constants.WriteCategory, constants.ReadCategory, constants.PubSubCategory}, - ExcludedCategories: []string{constants.AdminCategory, constants.ConnectionCategory, constants.DangerousCategory}, - IncludedCommands: []string{"acl|setuser", "acl|getuser", "acl|deluser"}, - ExcludedCommands: []string{"rewriteaof", "save", "acl|load", "acl|save"}, - IncludedReadKeys: []string{}, - IncludedWriteKeys: []string{}, - IncludedPubSubChannels: []string{"channel1", "channel2"}, - ExcludedPubSubChannels: []string{"channel3", "channel4"}, - }, - { - Username: "list_user_3", - Enabled: true, - NoPassword: false, - NoKeys: false, - Passwords: []acl.Password{ - {PasswordType: acl.PasswordPlainText, PasswordValue: "list_user_password_3"}, - {PasswordType: acl.PasswordSHA256, PasswordValue: generateSHA256Password("list_user_password_4")}, + { + Username: "list_user_2", + Enabled: true, + NoPassword: true, + NoKeys: true, + IncludeCategories: []string{constants.WriteCategory, constants.ReadCategory, constants.PubSubCategory}, + ExcludeCategories: []string{constants.AdminCategory, constants.ConnectionCategory, constants.DangerousCategory}, + IncludeCommands: []string{"acl|setuser", "acl|getuser", "acl|deluser"}, + ExcludeCommands: []string{"rewriteaof", "save", "acl|load", "acl|save"}, + IncludeReadKeys: []string{}, + IncludeWriteKeys: []string{}, + IncludeChannels: []string{"channel1", "channel2"}, + ExcludeChannels: []string{"channel3", "channel4"}, }, - IncludedCategories: []string{constants.WriteCategory, constants.ReadCategory, constants.PubSubCategory}, - ExcludedCategories: []string{constants.AdminCategory, constants.ConnectionCategory, constants.DangerousCategory}, - IncludedCommands: []string{"acl|setuser", "acl|getuser", "acl|deluser"}, - ExcludedCommands: []string{"rewriteaof", "save", "acl|load", "acl|save"}, - IncludedReadKeys: []string{"key1", "key2", "key3", "key4"}, - IncludedWriteKeys: []string{"key1", "key2", "key5", "key6"}, - IncludedPubSubChannels: []string{"channel1", "channel2"}, - ExcludedPubSubChannels: []string{"channel3", "channel4"}, - }, - }, - cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("LIST")}, - wantRes: []string{ - "default on +@all +all %RW~* +&*", - fmt.Sprintf("with_password_user on >password2 #%s +@all +all", generateSHA256Password("password3")), - "no_password_user on nopass >password4", - "disabled_user off >password5", - fmt.Sprintf(`list_user_1 on >list_user_password_1 #%s +@write +@read +@pubsub -@admin -@connection -@dangerous +acl|setuser +acl|getuser +acl|deluser -rewriteaof -save -acl|load -acl|save %s +&channel1 +&channel2 -&channel3 -&channel4`, generateSHA256Password("list_user_password_2"), "%RW~key1 %RW~key2 %R~key3 %R~key4"), - fmt.Sprintf(`list_user_2 on nopass nokeys +@write +@read +@pubsub -@admin -@connection -@dangerous +acl|setuser +acl|getuser +acl|deluser -rewriteaof -save -acl|load -acl|save +&channel1 +&channel2 -&channel3 -&channel4`), - fmt.Sprintf(`list_user_3 on >list_user_password_3 #%s +@write +@read +@pubsub -@admin -@connection -@dangerous +acl|setuser +acl|getuser +acl|deluser -rewriteaof -save -acl|load -acl|save %s +&channel1 +&channel2 -&channel3 -&channel4`, generateSHA256Password("list_user_password_4"), "%RW~key1 %RW~key2 %R~key3 %R~key4"), - }, - wantErr: "", - }, - } + { + Username: "list_user_3", + Enabled: true, + NoPassword: false, + NoKeys: false, + AddPlainPasswords: []string{"list_user_password_3"}, + AddHashPasswords: []string{generateSHA256Password("list_user_password_4")}, + IncludeCategories: []string{constants.WriteCategory, constants.ReadCategory, constants.PubSubCategory}, + ExcludeCategories: []string{constants.AdminCategory, constants.ConnectionCategory, constants.DangerousCategory}, + IncludeCommands: []string{"acl|setuser", "acl|getuser", "acl|deluser"}, + ExcludeCommands: []string{"rewriteaof", "save", "acl|load", "acl|save"}, + IncludeReadKeys: []string{"key1", "key2", "key3", "key4"}, + IncludeWriteKeys: []string{"key1", "key2", "key5", "key6"}, + IncludeChannels: []string{"channel1", "channel2"}, + ExcludeChannels: []string{"channel3", "channel4"}, + }, + }, + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("LIST")}, + wantRes: []string{ + "default on +@all +all %RW~* +&*", + fmt.Sprintf("with_password_user on >password2 #%s +@all +all %s~* +&*", + generateSHA256Password("password3"), "%RW"), + "no_password_user on nopass +@all +all %RW~* +&*", + "disabled_user off >password5 +@all +all %RW~* +&*", + fmt.Sprintf(`list_user_1 on >list_user_password_1 #%s +@write +@read +@pubsub -@admin -@connection -@dangerous +acl|setuser +acl|getuser +acl|deluser -rewriteaof -save -acl|load -acl|save %s +&channel1 +&channel2 -&channel3 -&channel4`, + generateSHA256Password("list_user_password_2"), "%RW~key1 %RW~key2 %R~key3 %R~key4 %W~key5 %W~key6"), + fmt.Sprintf(`list_user_2 on nopass nokeys +@write +@read +@pubsub -@admin -@connection -@dangerous +acl|setuser +acl|getuser +acl|deluser -rewriteaof -save -acl|load -acl|save +&channel1 +&channel2 -&channel3 -&channel4`), + fmt.Sprintf(`list_user_3 on >list_user_password_3 #%s +@write +@read +@pubsub -@admin -@connection -@dangerous +acl|setuser +acl|getuser +acl|deluser -rewriteaof -save -acl|load -acl|save %s +&channel1 +&channel2 -&channel3 -&channel4`, + generateSHA256Password("list_user_password_4"), "%RW~key1 %RW~key2 %R~key3 %R~key4 %W~key5 %W~key6"), + }, + wantErr: "", + }, + { + name: "2. Command too long", + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("LIST"), resp.StringValue("USERNAME")}, + wantRes: nil, + wantErr: constants.WrongArgsResponse, + }, + } - for _, test := range tests { - a.AddUsers(test.presetUsers) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetUsers != nil { + for _, user := range test.presetUsers { + if _, err := mockServer.ACLSetUser(*user); err != nil { + t.Error(err) + return + } + } + } - if err = r.WriteArray(test.cmd); err != nil { - t.Error(err) + if err = r.WriteArray(test.cmd); err != nil { + t.Error(err) + return + } + v, _, err := r.ReadValue() + if err != nil { + t.Error(err) + return + } + if test.wantErr != "" { + if !strings.Contains(v.Error().Error(), test.wantErr) { + t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, v.Error().Error()) + } + return + } + resArr := v.Array() + if len(resArr) != len(test.wantRes) { + t.Errorf("expected response of lenght %d, got lenght %d", len(test.wantRes), len(resArr)) + return + } + + var resStr []string + for i := 0; i < len(resArr); i++ { + resStr = strings.Split(resArr[i].String(), " ") + if !slices.ContainsFunc(test.wantRes, func(s string) bool { + expectedUserSlice := strings.Split(s, " ") + return compareSlices(resStr, expectedUserSlice) == nil + }) { + t.Errorf("could not find the following user in expected slice: %+v", resStr) + return + } + } + }) } - v, _, err := r.ReadValue() - if err != nil { - t.Error(err) + }) + + t.Run("Test_HandleSave", func(t *testing.T) { + t.Parallel() + + baseDir := path.Join(".", "testdata", "save") + + tests := []struct { + name string + path string + want []string // Response from ACL List command. + }{ + { + name: "1. Save ACL config to .json file", + path: path.Join(baseDir, "json_test.json"), + want: []string{ + "default on +@all +all %RW~* +&*", + fmt.Sprintf("with_password_user on >password2 #%s +@all +all %s~* +&*", + generateSHA256Password("password3"), "%RW"), + "no_password_user on nopass +@all +all %RW~* +&*", + "disabled_user off >password5 +@all +all %RW~* +&*", + }, + }, + { + name: "2. Save ACL config to .yaml file", + path: path.Join(baseDir, "yaml_test.yaml"), + want: []string{ + "default on +@all +all %RW~* +&*", + fmt.Sprintf("with_password_user on >password2 #%s +@all +all %s~* +&*", + generateSHA256Password("password3"), "%RW"), + "no_password_user on nopass +@all +all %RW~* +&*", + "disabled_user off >password5 +@all +all %RW~* +&*", + }, + }, + { + name: "3. Save ACL config to .yml file", + path: path.Join(baseDir, "yml_test.yml"), + want: []string{ + "default on +@all +all %RW~* +&*", + fmt.Sprintf("with_password_user on >password2 #%s +@all +all %s~* +&*", + generateSHA256Password("password3"), "%RW"), + "no_password_user on nopass +@all +all %RW~* +&*", + "disabled_user off >password5 +@all +all %RW~* +&*", + }, + }, } - if test.wantErr != "" { - if v.Error().Error() != test.wantErr { - t.Errorf("expected error response \"%s\", got \"%s\"", test.wantErr, v.Error().Error()) + + servers := make([]*echovault.EchoVault, len(tests)) + mut := sync.Mutex{} + t.Cleanup(func() { + _ = os.RemoveAll(baseDir) + for _, server := range servers { + if server != nil { + server.ShutDown() + } } - continue + }) + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + mut.Lock() + defer mut.Unlock() + // Get free port. + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + + // Create new server instance + mockServer, err := setUpServer(port, false, test.path) + if err != nil { + t.Error(err) + return + } + servers[i] = mockServer + go func() { + mockServer.Start() + }() + + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + client := resp.NewConn(conn) + + if err = client.WriteArray([]resp.Value{resp.StringValue("ACL"), resp.StringValue("SAVE")}); err != nil { + t.Error(err) + return + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + return + } + + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected OK response, got \"%s\"", res.String()) + return + } + + // Close client connection + if err = conn.Close(); err != nil { + t.Error(err) + return + } + + // Shutdown the mock server + mockServer.ShutDown() + + // Restart server and create new client connection + port, err = internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + mockServer, err = setUpServer(port, false, test.path) + if err != nil { + t.Error(err) + return + } + go func() { + mockServer.Start() + }() + + conn, err = internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + client = resp.NewConn(conn) + + if err = client.WriteArray([]resp.Value{resp.StringValue("ACL"), resp.StringValue("LIST")}); err != nil { + t.Error(err) + return + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + return + } + + // Check if ACL LIST returns the expected list of users. + resArr := res.Array() + if len(resArr) != len(test.want) { + t.Errorf("expected response of lenght %d, got lenght %d", len(test.want), len(resArr)) + return + } + + var resStr []string + for i := 0; i < len(resArr); i++ { + resStr = strings.Split(resArr[i].String(), " ") + if !slices.ContainsFunc(test.want, func(s string) bool { + expectedUserSlice := strings.Split(s, " ") + return compareSlices(resStr, expectedUserSlice) == nil + }) { + t.Errorf("could not find the following user in expected slice: %+v", resStr) + return + } + } + }) } - resArr := v.Array() - if len(resArr) != len(test.wantRes) { - t.Errorf("expected response of lenght %d, got lenght %d", len(test.wantRes), len(resArr)) + }) + + t.Run("Test_HandleLoad", func(t *testing.T) { + t.Parallel() + + baseDir := path.Join(".", "testdata", "load") + + tests := []struct { + name string + path string + users []echovault.User // Add users after server startup. + cmd []resp.Value // Command to load users from ACL config. + want []string + }{ + { + name: "1. Load config from the .json file", + path: path.Join(baseDir, "json_test.json"), + users: []echovault.User{ + {Username: "user1", Enabled: true}, + }, + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("LOAD"), resp.StringValue("REPLACE")}, + want: []string{ + "default on +@all +all %RW~* +&*", + fmt.Sprintf("with_password_user on >password2 #%s +@all +all %s~* +&*", + generateSHA256Password("password3"), "%RW"), + "no_password_user on nopass +@all +all %RW~* +&*", + "disabled_user off >password5 +@all +all %RW~* +&*", + "user1 on +@all +all %RW~* +&*", + }, + }, + { + name: "2. Load users from the .yaml file", + path: path.Join(baseDir, "yaml_test.yaml"), + users: []echovault.User{ + {Username: "user1", Enabled: true}, + }, + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("LOAD"), resp.StringValue("REPLACE")}, + want: []string{ + "default on +@all +all %RW~* +&*", + fmt.Sprintf("with_password_user on >password2 #%s +@all +all %s~* +&*", + generateSHA256Password("password3"), "%RW"), + "no_password_user on nopass +@all +all %RW~* +&*", + "disabled_user off >password5 +@all +all %RW~* +&*", + "user1 on +@all +all %RW~* +&*", + }, + }, + { + name: "3. Load users from the .yml file", + path: path.Join(baseDir, "yml_test.yml"), + users: []echovault.User{ + {Username: "user1", Enabled: true}, + }, + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("LOAD"), resp.StringValue("REPLACE")}, + want: []string{ + "default on +@all +all %RW~* +&*", + fmt.Sprintf("with_password_user on >password2 #%s +@all +all %s~* +&*", + generateSHA256Password("password3"), "%RW"), + "no_password_user on nopass +@all +all %RW~* +&*", + "disabled_user off >password5 +@all +all %RW~* +&*", + "user1 on +@all +all %RW~* +&*", + }, + }, + { + name: "4. Merge loaded users", + path: path.Join(baseDir, "merge.yml"), + users: []echovault.User{ + { // Disable user1. + Username: "user1", + Enabled: false, + }, + { // Update with_password_user. This should be merged with the existing user. + Username: "with_password_user", + AddPlainPasswords: []string{"password3", "password4"}, + IncludeReadWriteKeys: []string{"key1", "key2"}, + IncludeWriteKeys: []string{"key3", "key4"}, + IncludeReadKeys: []string{"key5", "key6"}, + IncludeChannels: []string{"channel[12]"}, + ExcludeChannels: []string{"channel[34]"}, + }, + }, + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("LOAD"), resp.StringValue("MERGE")}, + want: []string{ + "default on +@all +all %RW~* +&*", + fmt.Sprintf(`with_password_user on >password2 >password3 >password4 #%s +@all +all %s~key1 %s~key2 %s~key5 %s~key6 %s~key3 %s~key4 +&channel[12] -&channel[34]`, + generateSHA256Password("password3"), "%RW", "%RW", "%R", "%R", "%W", "%W"), + "no_password_user on nopass +@all +all %RW~* +&*", + "disabled_user off >password5 +@all +all %RW~* +&*", + "user1 off +@all +all %RW~* +&*", + }, + }, + { + name: "5. Replace loaded users", + path: path.Join(baseDir, "replace.yml"), + users: []echovault.User{ + { // Disable user1. + Username: "user1", + Enabled: false, + }, + { // Update with_password_user. This should be merged with the existing user. + Username: "with_password_user", + AddPlainPasswords: []string{"password3", "password4"}, + IncludeReadWriteKeys: []string{"key1", "key2"}, + IncludeWriteKeys: []string{"key3", "key4"}, + IncludeReadKeys: []string{"key5", "key6"}, + IncludeChannels: []string{"channel[12]"}, + ExcludeChannels: []string{"channel[34]"}, + }, + }, + cmd: []resp.Value{resp.StringValue("ACL"), resp.StringValue("LOAD"), resp.StringValue("REPLACE")}, + want: []string{ + "default on +@all +all %RW~* +&*", + fmt.Sprintf("with_password_user on >password2 #%s +@all +all %s~* +&*", + generateSHA256Password("password3"), "%RW"), + "no_password_user on nopass +@all +all %RW~* +&*", + "disabled_user off >password5 +@all +all %RW~* +&*", + "user1 off +@all +all %RW~* +&*", + }, + }, } - var resStr []string - for i := 0; i < len(resArr); i++ { - resStr = strings.Split(resArr[i].String(), " ") - if !slices.ContainsFunc(test.wantRes, func(s string) bool { - expectedUserSlice := strings.Split(s, " ") - return compareSlices(resStr, expectedUserSlice) == nil - }) { - t.Errorf("could not find the following user in expected slice: %+v", resStr) + + servers := make([]*echovault.EchoVault, len(tests)) + mut := sync.Mutex{} + t.Cleanup(func() { + _ = os.RemoveAll(baseDir) + for _, server := range servers { + if server != nil { + server.ShutDown() + } } - clear(resStr) + }) + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + mut.Lock() + defer mut.Unlock() + // Create server with pre-generated users. + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + mockServer, err := setUpServer(port, false, test.path) + if err != nil { + t.Error(err) + return + } + servers[i] = mockServer + go func() { + mockServer.Start() + }() + + // Save the current users to the ACL config file. + if _, err := mockServer.ACLSave(); err != nil { + t.Error(err) + return + } + + // Add some users to the ACL. + for _, user := range test.users { + if _, err := mockServer.ACLSetUser(user); err != nil { + t.Error(err) + return + } + } + + // Establish client connection + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + client := resp.NewConn(conn) + + // Load the users from the ACL config file. + if err := client.WriteArray(test.cmd); err != nil { + t.Error(err) + return + } + + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + return + } + + if !strings.EqualFold(res.String(), "ok") { + t.Error(err) + mockServer.ShutDown() + return + } + + // Get ACL List + if err = client.WriteArray([]resp.Value{resp.StringValue("ACL"), resp.StringValue("LIST")}); err != nil { + t.Error(err) + return + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + return + } + + // Check if ACL LIST returns the expected list of users. + resArr := res.Array() + if len(resArr) != len(test.want) { + t.Errorf("expected response of length %d, got lenght %d", len(test.want), len(resArr)) + return + } + + var resStr []string + for i := 0; i < len(resArr); i++ { + resStr = strings.Split(resArr[i].String(), " ") + if !slices.ContainsFunc(test.want, func(s string) bool { + expectedUserSlice := strings.Split(s, " ") + return compareSlices(resStr, expectedUserSlice) == nil + }) { + t.Errorf("could not find the following user in expected slice: %+v", resStr) + return + } + } + }) } - } + }) } diff --git a/internal/modules/acl/user.go b/internal/modules/acl/user.go index 948de1f6..a887a991 100644 --- a/internal/modules/acl/user.go +++ b/internal/modules/acl/user.go @@ -86,6 +86,15 @@ func (user *User) Normalise() { if slices.Contains(user.ExcludedPubSubChannels, "*") { user.IncludedPubSubChannels = []string{} } + + // Sort passwords + slices.SortStableFunc(user.Passwords, func(a, b Password) int { + types := map[string]int{ + PasswordPlainText: 0, + PasswordSHA256: 1, + } + return types[a.PasswordType] - types[b.PasswordType] + }) } func RemoveDuplicateEntries(entries []string, allAlias string) (res []string) { @@ -98,11 +107,13 @@ func RemoveDuplicateEntries(entries []string, allAlias string) (res []string) { entriesMap[entry] += 1 } for key, _ := range entriesMap { - if key == "*" { + if key == "*" && len(entriesMap) == 1 { res = []string{"*"} return } - res = append(res, key) + if key != "*" { + res = append(res, key) + } } return } @@ -127,19 +138,13 @@ func (user *User) UpdateUser(cmd []string) error { } if str[0] == '<' { user.Passwords = slices.DeleteFunc(user.Passwords, func(password Password) bool { - if strings.EqualFold(password.PasswordType, PasswordSHA256) { - return false - } - return password.PasswordValue == str[1:] + return strings.EqualFold(password.PasswordType, PasswordPlainText) && password.PasswordValue == str[1:] }) continue } if str[0] == '!' { user.Passwords = slices.DeleteFunc(user.Passwords, func(password Password) bool { - if strings.EqualFold(password.PasswordType, PasswordPlainText) { - return false - } - return password.PasswordValue == str[1:] + return strings.EqualFold(password.PasswordType, PasswordSHA256) && password.PasswordValue == str[1:] }) continue } @@ -241,8 +246,8 @@ func (user *User) UpdateUser(cmd []string) error { user.IncludedCategories = []string{} user.ExcludedCategories = []string{"*"} } - // If resetkeys is provided, reset all keys that the user can access - if strings.EqualFold(str, "resetkeys") { + // If resetkeys or nokeys is provided, reset all keys that the user can access. + if slices.Contains([]string{"resetkeys", "nokeys"}, str) { user.IncludedReadKeys = []string{} user.IncludedWriteKeys = []string{} user.NoKeys = true @@ -253,6 +258,7 @@ func (user *User) UpdateUser(cmd []string) error { user.ExcludedPubSubChannels = []string{"*"} } } + return nil } @@ -260,7 +266,6 @@ func (user *User) Merge(new *User) { user.Enabled = new.Enabled user.NoKeys = new.NoKeys user.NoPassword = new.NoPassword - user.Passwords = append(user.Passwords, new.Passwords...) user.IncludedCategories = append(user.IncludedCategories, new.IncludedCategories...) user.ExcludedCategories = append(user.ExcludedCategories, new.ExcludedCategories...) user.IncludedCommands = append(user.IncludedCommands, new.IncludedCommands...) @@ -269,6 +274,16 @@ func (user *User) Merge(new *User) { user.IncludedWriteKeys = append(user.IncludedWriteKeys, new.IncludedWriteKeys...) user.IncludedPubSubChannels = append(user.IncludedPubSubChannels, new.IncludedPubSubChannels...) user.ExcludedPubSubChannels = append(user.ExcludedPubSubChannels, new.ExcludedPubSubChannels...) + + // Add passwords. + for _, password := range new.Passwords { + if !slices.ContainsFunc(user.Passwords, func(p Password) bool { + return p.PasswordType == password.PasswordType && p.PasswordValue == password.PasswordValue + }) { + user.Passwords = append(user.Passwords, new.Passwords...) + } + } + user.Normalise() } diff --git a/internal/modules/admin/commands_test.go b/internal/modules/admin/commands_test.go index 68ccb600..a24519fb 100644 --- a/internal/modules/admin/commands_test.go +++ b/internal/modules/admin/commands_test.go @@ -15,8 +15,6 @@ package admin_test import ( - "bytes" - "context" "errors" "fmt" "github.com/echovault/echovault/echovault" @@ -33,15 +31,10 @@ import ( "github.com/echovault/echovault/internal/modules/sorted_set" str "github.com/echovault/echovault/internal/modules/string" "github.com/tidwall/resp" - "net" - "os" "path" - "reflect" "slices" "strings" - "sync" "testing" - "unsafe" ) func setupServer(port uint16) (*echovault.EchoVault, error) { @@ -53,74 +46,47 @@ func setupServer(port uint16) (*echovault.EchoVault, error) { return echovault.NewEchoVault(echovault.WithConfig(cfg)) } -func getUnexportedField(field reflect.Value) interface{} { - return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() -} - -func getHandler(mockServer *echovault.EchoVault, commands ...string) internal.HandlerFunc { - if len(commands) == 0 { - return nil - } - getCommands := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command) - for _, c := range getCommands() { - if strings.EqualFold(commands[0], c.Command) && len(commands) == 1 { - // Get command handler - return c.HandlerFunc - } - if strings.EqualFold(commands[0], c.Command) { - // Get sub-command handler - for _, sc := range c.SubCommands { - if strings.EqualFold(commands[1], sc.Command) { - return sc.HandlerFunc - } - } - } +func Test_AdminCommands(t *testing.T) { + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return } - return nil -} -func getHandlerFuncParams(ctx context.Context, mockServer *echovault.EchoVault, cmd []string, conn *net.Conn) internal.HandlerFuncParams { - getCommands := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command) - return internal.HandlerFuncParams{ - Context: ctx, - Command: cmd, - Connection: conn, - GetAllCommands: getCommands, + mockServer, err := setupServer(uint16(port)) + if err != nil { + t.Error(err) + return } -} -func Test_AdminCommand(t *testing.T) { + go func() { + mockServer.Start() + }() + t.Cleanup(func() { - _ = os.RemoveAll("./testdata") + mockServer.ShutDown() }) t.Run("Test COMMANDS command", func(t *testing.T) { - t.Parallel() - - port, err := internal.GetFreePort() - if err != nil { - t.Error(err) - return - } - mockServer, err := setupServer(uint16(port)) + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) return } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) - res, err := getHandler(mockServer, "COMMANDS")( - getHandlerFuncParams(context.Background(), mockServer, []string{"commands"}, nil), - ) - if err != nil { + if err = client.WriteArray([]resp.Value{resp.StringValue("COMMANDS")}); err != nil { t.Error(err) + return } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) + return } // Get all the commands from the existing modules. @@ -148,36 +114,31 @@ func Test_AdminCommand(t *testing.T) { } } - if len(allCommands) != len(rv.Array()) { - t.Errorf("expected commands list to be of length %d, got %d", len(allCommands), len(rv.Array())) + if len(allCommands) != len(res.Array()) { + t.Errorf("expected commands list to be of length %d, got %d", len(allCommands), len(res.Array())) } }) t.Run("Test COMMAND COUNT command", func(t *testing.T) { - t.Parallel() - - port, err := internal.GetFreePort() - if err != nil { - t.Error(err) - return - } - mockServer, err := setupServer(uint16(port)) + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) return } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) - res, err := getHandler(mockServer, "COMMAND", "COUNT")( - getHandlerFuncParams(context.Background(), mockServer, []string{"command", "count"}, nil), - ) - if err != nil { + if err = client.WriteArray([]resp.Value{resp.StringValue("COMMAND"), resp.StringValue("COUNT")}); err != nil { t.Error(err) + return } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) + return } // Get all the commands from the existing modules. @@ -205,25 +166,21 @@ func Test_AdminCommand(t *testing.T) { } } - if len(allCommands) != rv.Integer() { - t.Errorf("expected COMMAND COUNT to return %d, got %d", len(allCommands), rv.Integer()) + if len(allCommands) != res.Integer() { + t.Errorf("expected COMMAND COUNT to return %d, got %d", len(allCommands), res.Integer()) } }) t.Run("Test COMMAND LIST command", func(t *testing.T) { - t.Parallel() - - port, err := internal.GetFreePort() - if err != nil { - t.Error(err) - return - } - - mockServer, err := setupServer(uint16(port)) + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) return } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) // Get all the commands from the existing modules. var allCommands []internal.Command @@ -305,24 +262,26 @@ func Test_AdminCommand(t *testing.T) { } for _, test := range tests { - res, err := getHandler(mockServer, test.cmd...)( - getHandlerFuncParams(context.Background(), mockServer, test.cmd, nil), - ) - if err != nil { + command := make([]resp.Value, len(test.cmd)) + for i, c := range test.cmd { + command[i] = resp.StringValue(c) + } + if err = client.WriteArray(command); err != nil { t.Error(err) + return } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) + return } - if len(rv.Array()) != len(test.want) { - t.Errorf("expected response of length %d, got %d", len(test.want), len(rv.Array())) + if len(res.Array()) != len(test.want) { + t.Errorf("expected response of length %d, got %d", len(test.want), len(res.Array())) } - for _, command := range rv.Array() { + for _, command := range res.Array() { if !slices.ContainsFunc(test.want, func(c string) bool { return strings.EqualFold(c, command.String()) }) { @@ -333,17 +292,6 @@ func Test_AdminCommand(t *testing.T) { }) t.Run("Test MODULE LOAD command", func(t *testing.T) { - port, err := internal.GetFreePort() - if err != nil { - t.Error(err) - return - } - mockServer, err := setupServer(uint16(port)) - if err != nil { - t.Error(err) - return - } - tests := []struct { name string execCommand []resp.Value @@ -430,19 +378,14 @@ func Test_AdminCommand(t *testing.T) { }, } - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() - }() - wg.Wait() - - conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) + return } - + defer func() { + _ = conn.Close() + }() respConn := resp.NewConn(conn) for i := 0; i < len(tests); i++ { @@ -501,31 +444,14 @@ func Test_AdminCommand(t *testing.T) { }) t.Run("Test MODULE UNLOAD command", func(t *testing.T) { - port, err := internal.GetFreePort() - if err != nil { - t.Error(err) - return - } - mockServer, err := setupServer(uint16(port)) + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) return } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() + defer func() { + _ = conn.Close() }() - wg.Wait() - - conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) - if err != nil { - t.Error(err) - return - } - respConn := resp.NewConn(conn) // Load module.set module @@ -689,31 +615,14 @@ func Test_AdminCommand(t *testing.T) { }) t.Run("Test MODULE LIST command", func(t *testing.T) { - port, err := internal.GetFreePort() - if err != nil { - t.Error(err) - return - } - mockServer, err := setupServer(uint16(port)) + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) return } - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() + defer func() { + _ = conn.Close() }() - wg.Wait() - - conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) - if err != nil { - t.Error(err) - return - } - respConn := resp.NewConn(conn) // Load module.get module with arg diff --git a/internal/modules/connection/commands_test.go b/internal/modules/connection/commands_test.go index 0ab5962b..cbd3694e 100644 --- a/internal/modules/connection/commands_test.go +++ b/internal/modules/connection/commands_test.go @@ -15,110 +15,103 @@ package connection_test import ( - "bytes" - "context" "errors" "github.com/echovault/echovault/echovault" "github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/constants" "github.com/tidwall/resp" - "net" - "reflect" "strings" "testing" - "unsafe" ) -var mockServer *echovault.EchoVault +func Test_Connection(t *testing.T) { + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } -func init() { - mockServer, _ = echovault.NewEchoVault( + mockServer, err := echovault.NewEchoVault( echovault.WithConfig(config.Config{ DataDir: "", EvictionPolicy: constants.NoEviction, + BindAddr: "localhost", + Port: uint16(port), }), ) -} + if err != nil { + t.Error(err) + return + } -func getUnexportedField(field reflect.Value) interface{} { - return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() -} + go func() { + mockServer.Start() + }() -func getHandler(commands ...string) internal.HandlerFunc { - if len(commands) == 0 { - return nil - } - getCommands := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command) - for _, c := range getCommands() { - if strings.EqualFold(commands[0], c.Command) && len(commands) == 1 { - // Get command handler - return c.HandlerFunc + t.Cleanup(func() { + mockServer.ShutDown() + }) + + t.Run("Test_HandlePing", func(t *testing.T) { + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return } - if strings.EqualFold(commands[0], c.Command) { - // Get sub-command handler - for _, sc := range c.SubCommands { - if strings.EqualFold(commands[1], sc.Command) { - return sc.HandlerFunc - } - } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + command []resp.Value + expected string + expectedErr error + }{ + { + command: []resp.Value{resp.StringValue("PING")}, + expected: "PONG", + expectedErr: nil, + }, + { + command: []resp.Value{resp.StringValue("PING"), resp.StringValue("Hello, world!")}, + expected: "Hello, world!", + expectedErr: nil, + }, + { + command: []resp.Value{ + resp.StringValue("PING"), + resp.StringValue("Hello, world!"), + resp.StringValue("Once more"), + }, + expected: "", + expectedErr: errors.New(constants.WrongArgsResponse), + }, } - } - return nil -} -func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn) internal.HandlerFuncParams { - return internal.HandlerFuncParams{ - Context: ctx, - Command: cmd, - Connection: conn, - } -} + for _, test := range tests { + if err = client.WriteArray(test.command); err != nil { + t.Error(err) + return + } -func Test_HandlePing(t *testing.T) { - ctx := context.Background() + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - tests := []struct { - command []string - expected string - expectedErr error - }{ - { - command: []string{"PING"}, - expected: "PONG", - expectedErr: nil, - }, - { - command: []string{"PING", "Hello, world!"}, - expected: "Hello, world!", - expectedErr: nil, - }, - { - command: []string{"PING", "Hello, world!", "Once more"}, - expected: "", - expectedErr: errors.New(constants.WrongArgsResponse), - }, - } + if test.expectedErr != nil { + if !strings.Contains(res.Error().Error(), test.expectedErr.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedErr.Error(), res.Error().Error()) + } + continue + } - for _, test := range tests { - res, err := getHandler("PING")(getHandlerFuncParams(ctx, test.command, nil)) - if test.expectedErr != nil && err != nil { - if err.Error() != test.expectedErr.Error() { - t.Errorf("expected error %s, got: %s", test.expectedErr.Error(), err.Error()) + if res.String() != test.expected { + t.Errorf("expected response \"%s\", got \"%s\"", test.expected, res.String()) } - continue - } - if err != nil { - t.Error(err) - } - rd := resp.NewReader(bytes.NewBuffer(res)) - v, _, err := rd.ReadValue() - if err != nil { - t.Error(err) } - if v.String() != test.expected { - t.Errorf("expected %s, got: %s", test.expected, v.String()) - } - } + }) + } diff --git a/internal/modules/generic/commands_test.go b/internal/modules/generic/commands_test.go index 84f8c8ae..9d091dac 100644 --- a/internal/modules/generic/commands_test.go +++ b/internal/modules/generic/commands_test.go @@ -23,930 +23,716 @@ import ( "github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/constants" "github.com/tidwall/resp" - "net" "strings" - "sync" "testing" "time" ) -var addr string -var port int -var mockServer *echovault.EchoVault -var mockClock clock.Clock - type KeyData struct { Value interface{} ExpireAt time.Time } -func init() { - mockClock = clock.NewClock() - port, _ = internal.GetFreePort() - mockServer, _ = echovault.NewEchoVault( +func Test_Generic(t *testing.T) { + mockClock := clock.NewClock() + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + + mockServer, err := echovault.NewEchoVault( echovault.WithConfig(config.Config{ - BindAddr: addr, + BindAddr: "localhost", Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() - }() - wg.Wait() -} - -func Test_HandleSET(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) if err != nil { t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - command []string - presetValues map[string]KeyData - expectedResponse interface{} - expectedValue interface{} - expectedExpiry time.Time - expectedErr error - }{ - { - name: "1. Set normal string value", - command: []string{"SET", "SetKey1", "value1"}, - presetValues: nil, - expectedResponse: "OK", - expectedValue: "value1", - expectedExpiry: time.Time{}, - expectedErr: nil, - }, - { - name: "2. Set normal integer value", - command: []string{"SET", "SetKey2", "1245678910"}, - presetValues: nil, - expectedResponse: "OK", - expectedValue: "1245678910", - expectedExpiry: time.Time{}, - expectedErr: nil, - }, - { - name: "3. Set normal float value", - command: []string{"SET", "SetKey3", "45782.11341"}, - presetValues: nil, - expectedResponse: "OK", - expectedValue: "45782.11341", - expectedExpiry: time.Time{}, - expectedErr: nil, - }, - { - name: "4. Only set the value if the key does not exist", - command: []string{"SET", "SetKey4", "value4", "NX"}, - presetValues: nil, - expectedResponse: "OK", - expectedValue: "value4", - expectedExpiry: time.Time{}, - expectedErr: nil, - }, - { - name: "5. Throw error when value already exists with NX flag passed", - command: []string{"SET", "SetKey5", "value5", "NX"}, - presetValues: map[string]KeyData{ - "SetKey5": { - Value: "preset-value5", - ExpireAt: time.Time{}, - }, - }, - expectedResponse: nil, - expectedValue: "preset-value5", - expectedExpiry: time.Time{}, - expectedErr: errors.New("key SetKey5 already exists"), - }, - { - name: "6. Set new key value when key exists with XX flag passed", - command: []string{"SET", "SetKey6", "value6", "XX"}, - presetValues: map[string]KeyData{ - "SetKey6": { - Value: "preset-value6", - ExpireAt: time.Time{}, - }, - }, - expectedResponse: "OK", - expectedValue: "value6", - expectedExpiry: time.Time{}, - expectedErr: nil, - }, - { - name: "7. Return error when setting non-existent key with XX flag", - command: []string{"SET", "SetKey7", "value7", "XX"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: nil, - expectedExpiry: time.Time{}, - expectedErr: errors.New("key SetKey7 does not exist"), - }, - { - name: "8. Return error when NX flag is provided after XX flag", - command: []string{"SET", "SetKey8", "value8", "XX", "NX"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: nil, - expectedExpiry: time.Time{}, - expectedErr: errors.New("cannot specify NX when XX is already specified"), - }, - { - name: "9. Return error when XX flag is provided after NX flag", - command: []string{"SET", "SetKey9", "value9", "NX", "XX"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: nil, - expectedExpiry: time.Time{}, - expectedErr: errors.New("cannot specify XX when NX is already specified"), - }, - { - name: "10. Set expiry time on the key to 100 seconds from now", - command: []string{"SET", "SetKey10", "value10", "EX", "100"}, - presetValues: nil, - expectedResponse: "OK", - expectedValue: "value10", - expectedExpiry: mockClock.Now().Add(100 * time.Second), - expectedErr: nil, - }, - { - name: "11. Return error when EX flag is passed without seconds value", - command: []string{"SET", "SetKey11", "value11", "EX"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: "", - expectedExpiry: time.Time{}, - expectedErr: errors.New("seconds value required after EX"), - }, - { - name: "12. Return error when EX flag is passed with invalid (non-integer) value", - command: []string{"SET", "SetKey12", "value12", "EX", "seconds"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: "", - expectedExpiry: time.Time{}, - expectedErr: errors.New("seconds value should be an integer"), - }, - { - name: "13. Return error when trying to set expiry seconds when expiry is already set", - command: []string{"SET", "SetKey13", "value13", "PX", "100000", "EX", "100"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: nil, - expectedExpiry: time.Time{}, - expectedErr: errors.New("cannot specify EX when expiry time is already set"), - }, - { - name: "14. Set expiry time on the key in unix milliseconds", - command: []string{"SET", "SetKey14", "value14", "PX", "4096"}, - presetValues: nil, - expectedResponse: "OK", - expectedValue: "value14", - expectedExpiry: mockClock.Now().Add(4096 * time.Millisecond), - expectedErr: nil, - }, - { - name: "15. Return error when PX flag is passed without milliseconds value", - command: []string{"SET", "SetKey15", "value15", "PX"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: "", - expectedExpiry: time.Time{}, - expectedErr: errors.New("milliseconds value required after PX"), - }, - { - name: "16. Return error when PX flag is passed with invalid (non-integer) value", - command: []string{"SET", "SetKey16", "value16", "PX", "milliseconds"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: "", - expectedExpiry: time.Time{}, - expectedErr: errors.New("milliseconds value should be an integer"), - }, - { - name: "17. Return error when trying to set expiry milliseconds when expiry is already provided", - command: []string{"SET", "SetKey17", "value17", "EX", "10", "PX", "1000000"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: nil, - expectedExpiry: time.Time{}, - expectedErr: errors.New("cannot specify PX when expiry time is already set"), - }, - { - name: "18. Set exact expiry time in seconds from unix epoch", - command: []string{ - "SET", "SetKey18", "value18", - "EXAT", fmt.Sprintf("%d", mockClock.Now().Add(200*time.Second).Unix()), - }, - presetValues: nil, - expectedResponse: "OK", - expectedValue: "value18", - expectedExpiry: mockClock.Now().Add(200 * time.Second), - expectedErr: nil, - }, - { - name: "19. Return error when trying to set exact seconds expiry time when expiry time is already provided", - command: []string{ - "SET", "SetKey19", "value19", - "EX", "10", - "EXAT", fmt.Sprintf("%d", mockClock.Now().Add(200*time.Second).Unix()), - }, - presetValues: nil, - expectedResponse: nil, - expectedValue: "", - expectedExpiry: time.Time{}, - expectedErr: errors.New("cannot specify EXAT when expiry time is already set"), - }, - { - name: "20. Return error when no seconds value is provided after EXAT flag", - command: []string{"SET", "SetKey20", "value20", "EXAT"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: "", - expectedExpiry: time.Time{}, - expectedErr: errors.New("seconds value required after EXAT"), - }, - { - name: "21. Return error when invalid (non-integer) value is passed after EXAT flag", - command: []string{"SET", "SekKey21", "value21", "EXAT", "seconds"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: "", - expectedExpiry: time.Time{}, - expectedErr: errors.New("seconds value should be an integer"), - }, - { - name: "22. Set exact expiry time in milliseconds from unix epoch", - command: []string{ - "SET", "SetKey22", "value22", - "PXAT", fmt.Sprintf("%d", mockClock.Now().Add(4096*time.Millisecond).UnixMilli()), - }, - presetValues: nil, - expectedResponse: "OK", - expectedValue: "value22", - expectedExpiry: mockClock.Now().Add(4096 * time.Millisecond), - expectedErr: nil, - }, - { - name: "23. Return error when trying to set exact milliseconds expiry time when expiry time is already provided", - command: []string{ - "SET", "SetKey23", "value23", - "PX", "1000", - "PXAT", fmt.Sprintf("%d", mockClock.Now().Add(4096*time.Millisecond).UnixMilli()), - }, - presetValues: nil, - expectedResponse: nil, - expectedValue: "", - expectedExpiry: time.Time{}, - expectedErr: errors.New("cannot specify PXAT when expiry time is already set"), - }, - { - name: "24. Return error when no milliseconds value is provided after PXAT flag", - command: []string{"SET", "SetKey24", "value24", "PXAT"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: "", - expectedExpiry: time.Time{}, - expectedErr: errors.New("milliseconds value required after PXAT"), - }, - { - name: "25. Return error when invalid (non-integer) value is passed after EXAT flag", - command: []string{"SET", "SetKey25", "value25", "PXAT", "unix-milliseconds"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: "", - expectedExpiry: time.Time{}, - expectedErr: errors.New("milliseconds value should be an integer"), - }, - { - name: "26. Get the previous value when GET flag is passed", - command: []string{"SET", "SetKey26", "value26", "GET", "EX", "1000"}, - presetValues: map[string]KeyData{ - "SetKey26": { - Value: "previous-value", - ExpireAt: time.Time{}, - }, - }, - expectedResponse: "previous-value", - expectedValue: "value26", - expectedExpiry: mockClock.Now().Add(1000 * time.Second), - expectedErr: nil, - }, - { - name: "27. Return nil when GET value is passed and no previous value exists", - command: []string{"SET", "SetKey27", "value27", "GET", "EX", "1000"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: "value27", - expectedExpiry: mockClock.Now().Add(1000 * time.Second), - expectedErr: nil, - }, - { - name: "28. Throw error when unknown optional flag is passed to SET command.", - command: []string{"SET", "SetKey28", "value28", "UNKNOWN-OPTION"}, - presetValues: nil, - expectedResponse: nil, - expectedValue: nil, - expectedExpiry: time.Time{}, - expectedErr: errors.New("unknown option UNKNOWN-OPTION for set command"), - }, - { - name: "29. Command too short", - command: []string{"SET"}, - expectedResponse: nil, - expectedValue: nil, - expectedErr: errors.New(constants.WrongArgsResponse), - }, - { - name: "30. Command too long", - command: []string{"SET", "SetKey30", "value1", "value2", "value3", "value4", "value5", "value6"}, - expectedResponse: nil, - expectedValue: nil, - expectedErr: errors.New(constants.WrongArgsResponse), - }, + return } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - for k, v := range test.presetValues { - cmd := []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(k), - resp.StringValue(v.Value.(string))} - err := client.WriteArray(cmd) - if err != nil { - t.Error(err) - } - rd, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - if !strings.EqualFold(rd.String(), "ok") { - t.Errorf("expected preset response to be \"OK\", got %s", rd.String()) + go func() { + mockServer.Start() + }() + + t.Cleanup(func() { + mockServer.ShutDown() + }) + + t.Run("Test_HandleSET", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + command []string + presetValues map[string]KeyData + expectedResponse interface{} + expectedValue interface{} + expectedExpiry time.Time + expectedErr error + }{ + { + name: "1. Set normal string value", + command: []string{"SET", "SetKey1", "value1"}, + presetValues: nil, + expectedResponse: "OK", + expectedValue: "value1", + expectedExpiry: time.Time{}, + expectedErr: nil, + }, + { + name: "2. Set normal integer value", + command: []string{"SET", "SetKey2", "1245678910"}, + presetValues: nil, + expectedResponse: "OK", + expectedValue: "1245678910", + expectedExpiry: time.Time{}, + expectedErr: nil, + }, + { + name: "3. Set normal float value", + command: []string{"SET", "SetKey3", "45782.11341"}, + presetValues: nil, + expectedResponse: "OK", + expectedValue: "45782.11341", + expectedExpiry: time.Time{}, + expectedErr: nil, + }, + { + name: "4. Only set the value if the key does not exist", + command: []string{"SET", "SetKey4", "value4", "NX"}, + presetValues: nil, + expectedResponse: "OK", + expectedValue: "value4", + expectedExpiry: time.Time{}, + expectedErr: nil, + }, + { + name: "5. Throw error when value already exists with NX flag passed", + command: []string{"SET", "SetKey5", "value5", "NX"}, + presetValues: map[string]KeyData{ + "SetKey5": { + Value: "preset-value5", + ExpireAt: time.Time{}, + }, + }, + expectedResponse: nil, + expectedValue: "preset-value5", + expectedExpiry: time.Time{}, + expectedErr: errors.New("key SetKey5 already exists"), + }, + { + name: "6. Set new key value when key exists with XX flag passed", + command: []string{"SET", "SetKey6", "value6", "XX"}, + presetValues: map[string]KeyData{ + "SetKey6": { + Value: "preset-value6", + ExpireAt: time.Time{}, + }, + }, + expectedResponse: "OK", + expectedValue: "value6", + expectedExpiry: time.Time{}, + expectedErr: nil, + }, + { + name: "7. Return error when setting non-existent key with XX flag", + command: []string{"SET", "SetKey7", "value7", "XX"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: nil, + expectedExpiry: time.Time{}, + expectedErr: errors.New("key SetKey7 does not exist"), + }, + { + name: "8. Return error when NX flag is provided after XX flag", + command: []string{"SET", "SetKey8", "value8", "XX", "NX"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: nil, + expectedExpiry: time.Time{}, + expectedErr: errors.New("cannot specify NX when XX is already specified"), + }, + { + name: "9. Return error when XX flag is provided after NX flag", + command: []string{"SET", "SetKey9", "value9", "NX", "XX"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: nil, + expectedExpiry: time.Time{}, + expectedErr: errors.New("cannot specify XX when NX is already specified"), + }, + { + name: "10. Set expiry time on the key to 100 seconds from now", + command: []string{"SET", "SetKey10", "value10", "EX", "100"}, + presetValues: nil, + expectedResponse: "OK", + expectedValue: "value10", + expectedExpiry: mockClock.Now().Add(100 * time.Second), + expectedErr: nil, + }, + { + name: "11. Return error when EX flag is passed without seconds value", + command: []string{"SET", "SetKey11", "value11", "EX"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: "", + expectedExpiry: time.Time{}, + expectedErr: errors.New("seconds value required after EX"), + }, + { + name: "12. Return error when EX flag is passed with invalid (non-integer) value", + command: []string{"SET", "SetKey12", "value12", "EX", "seconds"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: "", + expectedExpiry: time.Time{}, + expectedErr: errors.New("seconds value should be an integer"), + }, + { + name: "13. Return error when trying to set expiry seconds when expiry is already set", + command: []string{"SET", "SetKey13", "value13", "PX", "100000", "EX", "100"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: nil, + expectedExpiry: time.Time{}, + expectedErr: errors.New("cannot specify EX when expiry time is already set"), + }, + { + name: "14. Set expiry time on the key in unix milliseconds", + command: []string{"SET", "SetKey14", "value14", "PX", "4096"}, + presetValues: nil, + expectedResponse: "OK", + expectedValue: "value14", + expectedExpiry: mockClock.Now().Add(4096 * time.Millisecond), + expectedErr: nil, + }, + { + name: "15. Return error when PX flag is passed without milliseconds value", + command: []string{"SET", "SetKey15", "value15", "PX"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: "", + expectedExpiry: time.Time{}, + expectedErr: errors.New("milliseconds value required after PX"), + }, + { + name: "16. Return error when PX flag is passed with invalid (non-integer) value", + command: []string{"SET", "SetKey16", "value16", "PX", "milliseconds"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: "", + expectedExpiry: time.Time{}, + expectedErr: errors.New("milliseconds value should be an integer"), + }, + { + name: "17. Return error when trying to set expiry milliseconds when expiry is already provided", + command: []string{"SET", "SetKey17", "value17", "EX", "10", "PX", "1000000"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: nil, + expectedExpiry: time.Time{}, + expectedErr: errors.New("cannot specify PX when expiry time is already set"), + }, + { + name: "18. Set exact expiry time in seconds from unix epoch", + command: []string{ + "SET", "SetKey18", "value18", + "EXAT", fmt.Sprintf("%d", mockClock.Now().Add(200*time.Second).Unix()), + }, + presetValues: nil, + expectedResponse: "OK", + expectedValue: "value18", + expectedExpiry: mockClock.Now().Add(200 * time.Second), + expectedErr: nil, + }, + { + name: "19. Return error when trying to set exact seconds expiry time when expiry time is already provided", + command: []string{ + "SET", "SetKey19", "value19", + "EX", "10", + "EXAT", fmt.Sprintf("%d", mockClock.Now().Add(200*time.Second).Unix()), + }, + presetValues: nil, + expectedResponse: nil, + expectedValue: "", + expectedExpiry: time.Time{}, + expectedErr: errors.New("cannot specify EXAT when expiry time is already set"), + }, + { + name: "20. Return error when no seconds value is provided after EXAT flag", + command: []string{"SET", "SetKey20", "value20", "EXAT"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: "", + expectedExpiry: time.Time{}, + expectedErr: errors.New("seconds value required after EXAT"), + }, + { + name: "21. Return error when invalid (non-integer) value is passed after EXAT flag", + command: []string{"SET", "SekKey21", "value21", "EXAT", "seconds"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: "", + expectedExpiry: time.Time{}, + expectedErr: errors.New("seconds value should be an integer"), + }, + { + name: "22. Set exact expiry time in milliseconds from unix epoch", + command: []string{ + "SET", "SetKey22", "value22", + "PXAT", fmt.Sprintf("%d", mockClock.Now().Add(4096*time.Millisecond).UnixMilli()), + }, + presetValues: nil, + expectedResponse: "OK", + expectedValue: "value22", + expectedExpiry: mockClock.Now().Add(4096 * time.Millisecond), + expectedErr: nil, + }, + { + name: "23. Return error when trying to set exact milliseconds expiry time when expiry time is already provided", + command: []string{ + "SET", "SetKey23", "value23", + "PX", "1000", + "PXAT", fmt.Sprintf("%d", mockClock.Now().Add(4096*time.Millisecond).UnixMilli()), + }, + presetValues: nil, + expectedResponse: nil, + expectedValue: "", + expectedExpiry: time.Time{}, + expectedErr: errors.New("cannot specify PXAT when expiry time is already set"), + }, + { + name: "24. Return error when no milliseconds value is provided after PXAT flag", + command: []string{"SET", "SetKey24", "value24", "PXAT"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: "", + expectedExpiry: time.Time{}, + expectedErr: errors.New("milliseconds value required after PXAT"), + }, + { + name: "25. Return error when invalid (non-integer) value is passed after EXAT flag", + command: []string{"SET", "SetKey25", "value25", "PXAT", "unix-milliseconds"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: "", + expectedExpiry: time.Time{}, + expectedErr: errors.New("milliseconds value should be an integer"), + }, + { + name: "26. Get the previous value when GET flag is passed", + command: []string{"SET", "SetKey26", "value26", "GET", "EX", "1000"}, + presetValues: map[string]KeyData{ + "SetKey26": { + Value: "previous-value", + ExpireAt: time.Time{}, + }, + }, + expectedResponse: "previous-value", + expectedValue: "value26", + expectedExpiry: mockClock.Now().Add(1000 * time.Second), + expectedErr: nil, + }, + { + name: "27. Return nil when GET value is passed and no previous value exists", + command: []string{"SET", "SetKey27", "value27", "GET", "EX", "1000"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: "value27", + expectedExpiry: mockClock.Now().Add(1000 * time.Second), + expectedErr: nil, + }, + { + name: "28. Throw error when unknown optional flag is passed to SET command.", + command: []string{"SET", "SetKey28", "value28", "UNKNOWN-OPTION"}, + presetValues: nil, + expectedResponse: nil, + expectedValue: nil, + expectedExpiry: time.Time{}, + expectedErr: errors.New("unknown option UNKNOWN-OPTION for set command"), + }, + { + name: "29. Command too short", + command: []string{"SET"}, + expectedResponse: nil, + expectedValue: nil, + expectedErr: errors.New(constants.WrongArgsResponse), + }, + { + name: "30. Command too long", + command: []string{"SET", "SetKey30", "value1", "value2", "value3", "value4", "value5", "value6"}, + expectedResponse: nil, + expectedValue: nil, + expectedErr: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + for k, v := range test.presetValues { + cmd := []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(k), + resp.StringValue(v.Value.(string))} + err := client.WriteArray(cmd) + if err != nil { + t.Error(err) + } + rd, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + if !strings.EqualFold(rd.String(), "ok") { + t.Errorf("expected preset response to be \"OK\", got %s", rd.String()) + } } } - } - - command := make([]resp.Value, len(test.command)) - for j, c := range test.command { - command[j] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - - res, _, err := client.ReadValue() - - if test.expectedErr != nil { - if !strings.Contains(res.Error().Error(), test.expectedErr.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedErr.Error(), err.Error()) - } - return - } - if err != nil { - t.Error(err) - } - - switch test.expectedResponse.(type) { - case string: - if test.expectedResponse != res.String() { - t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) - } - case nil: - if !res.IsNull() { - t.Errorf("expcted nil response, got %+v", res) - } - default: - t.Error("test expected result should be nil or string") - } - - key := test.command[1] - - // Compare expected value to response value - if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - if res.String() != test.expectedValue.(string) { - t.Errorf("expected value %s, got %s", test.expectedValue.(string), res.String()) - } - - // Compare expected expiry to response expiry - if !test.expectedExpiry.Equal(time.Time{}) { - if err = client.WriteArray([]resp.Value{resp.StringValue("EXPIRETIME"), resp.StringValue(key)}); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - if res.Integer() != int(test.expectedExpiry.Unix()) { - t.Errorf("expected expiry time %d, got %d", test.expectedExpiry.Unix(), res.Integer()) - } - } - }) - } -} -func Test_HandleMSET(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - command []string - expectedResponse string - expectedValues map[string]interface{} - expectedErr error - }{ - { - name: "1. Set multiple key value pairs", - command: []string{"MSET", "MsetKey1", "value1", "MsetKey2", "10", "MsetKey3", "3.142"}, - expectedResponse: "OK", - expectedValues: map[string]interface{}{"MsetKey1": "value1", "MsetKey2": 10, "MsetKey3": 3.142}, - expectedErr: nil, - }, - { - name: "2. Return error when keys and values are not even", - command: []string{"MSET", "MsetKey1", "value1", "MsetKey2", "10", "MsetKey3"}, - expectedResponse: "", - expectedValues: make(map[string]interface{}), - expectedErr: errors.New("each key must be paired with a value"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - command := make([]resp.Value, len(test.command)) - for j, c := range test.command { - command[j] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedErr != nil { - if !strings.Contains(res.Error().Error(), test.expectedErr.Error()) { - t.Errorf("expected error %s, got %s", test.expectedErr.Error(), err.Error()) + command := make([]resp.Value, len(test.command)) + for j, c := range test.command { + command[j] = resp.StringValue(c) } - return - } - - if res.String() != test.expectedResponse { - t.Errorf("expected response %s, got %s", test.expectedResponse, res.String()) - } - for key, expectedValue := range test.expectedValues { - // Get value from server - if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - res, _, err = client.ReadValue() - switch expectedValue.(type) { - default: - t.Error("unexpected type for expectedValue") - case int: - ev, _ := expectedValue.(int) - if res.Integer() != ev { - t.Errorf("expected value %d for key %s, got %d", ev, key, res.Integer()) - } - case float64: - ev, _ := expectedValue.(float64) - if res.Float() != ev { - t.Errorf("expected value %f for key %s, got %f", ev, key, res.Float()) - } - case string: - ev, _ := expectedValue.(string) - if res.String() != ev { - t.Errorf("expected value %s for key %s, got %s", ev, key, res.String()) + + res, _, err := client.ReadValue() + + if test.expectedErr != nil { + if !strings.Contains(res.Error().Error(), test.expectedErr.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedErr.Error(), err.Error()) } + return } - } - }) - } -} - -func Test_HandleGET(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - value string - }{ - { - name: "1. String", - key: "GetKey1", - value: "value1", - }, - { - name: "2. Integer", - key: "GetKey2", - value: "10", - }, - { - name: "3. Float", - key: "GetKey3", - value: "3.142", - }, - } - // Test successful Get command - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - func(key, value string) { - // Preset the values - err = client.WriteArray([]resp.Value{resp.StringValue("SET"), resp.StringValue(key), resp.StringValue(value)}) if err != nil { t.Error(err) } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) + switch test.expectedResponse.(type) { + case string: + if test.expectedResponse != res.String() { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) + } + case nil: + if !res.IsNull() { + t.Errorf("expcted nil response, got %+v", res) + } + default: + t.Error("test expected result should be nil or string") } - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected preset response to be \"OK\", got %s", res.String()) - } + key := test.command[1] + // Compare expected value to response value if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { t.Error(err) } - res, _, err = client.ReadValue() if err != nil { t.Error(err) } - - if res.String() != test.value { - t.Errorf("expected value %s, got %s", test.value, res.String()) + if res.String() != test.expectedValue.(string) { + t.Errorf("expected value %s, got %s", test.expectedValue.(string), res.String()) } - }(test.key, test.value) - }) - } - // Test get non-existent key - if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue("test4")}); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - if !res.IsNull() { - t.Errorf("expected nil, got: %+v", res) - } - - errorTests := []struct { - name string - command []string - expected string - }{ - { - name: "1. Return error when no GET key is passed", - command: []string{"GET"}, - expected: constants.WrongArgsResponse, - }, - { - name: "2. Return error when too many GET keys are passed", - command: []string{"GET", "GetKey1", "test"}, - expected: constants.WrongArgsResponse, - }, - } - for _, test := range errorTests { - t.Run(test.name, func(t *testing.T) { - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if !strings.Contains(res.Error().Error(), test.expected) { - t.Errorf("expected error '%s', got: %s", test.expected, err.Error()) - } - }) - } -} - -func Test_HandleMGET(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetKeys []string - presetValues []string - command []string - expected []interface{} - expectedError error - }{ - { - name: "1. MGET multiple existing values", - presetKeys: []string{"MgetKey1", "MgetKey2", "MgetKey3", "MgetKey4"}, - presetValues: []string{"value1", "value2", "value3", "value4"}, - command: []string{"MGET", "MgetKey1", "MgetKey4", "MgetKey2", "MgetKey3", "MgetKey1"}, - expected: []interface{}{"value1", "value4", "value2", "value3", "value1"}, - expectedError: nil, - }, - { - name: "2. MGET multiple values with nil values spliced in", - presetKeys: []string{"MgetKey5", "MgetKey6", "MgetKey7"}, - presetValues: []string{"value5", "value6", "value7"}, - command: []string{"MGET", "MgetKey5", "MgetKey6", "non-existent", "non-existent", "MgetKey7", "non-existent"}, - expected: []interface{}{"value5", "value6", nil, nil, "value7", nil}, - expectedError: nil, - }, - { - name: "3. Return error when MGET is invoked with no keys", - presetKeys: []string{"MgetKey5"}, - presetValues: []string{"value5"}, - command: []string{"MGET"}, - expected: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + // Compare expected expiry to response expiry + if !test.expectedExpiry.Equal(time.Time{}) { + if err = client.WriteArray([]resp.Value{resp.StringValue("EXPIRETIME"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if res.Integer() != int(test.expectedExpiry.Unix()) { + t.Errorf("expected expiry time %d, got %d", test.expectedExpiry.Unix(), res.Integer()) + } + } + }) + } + }) + + t.Run("Test_HandleMSET", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + command []string + expectedResponse string + expectedValues map[string]interface{} + expectedErr error + }{ + { + name: "1. Set multiple key value pairs", + command: []string{"MSET", "MsetKey1", "value1", "MsetKey2", "10", "MsetKey3", "3.142"}, + expectedResponse: "OK", + expectedValues: map[string]interface{}{"MsetKey1": "value1", "MsetKey2": 10, "MsetKey3": 3.142}, + expectedErr: nil, + }, + { + name: "2. Return error when keys and values are not even", + command: []string{"MSET", "MsetKey1", "value1", "MsetKey2", "10", "MsetKey3"}, + expectedResponse: "", + expectedValues: make(map[string]interface{}), + expectedErr: errors.New("each key must be paired with a value"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + command := make([]resp.Value, len(test.command)) + for j, c := range test.command { + command[j] = resp.StringValue(c) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // Set up the values - for i, key := range test.presetKeys { - if err = client.WriteArray([]resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(test.presetValues[i]), - }); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected preset response to be \"OK\", got \"%s\"", res.String()) - } - } - - // Test the command and its results - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - // If we expect and error, branch out and check error - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error %+v, got: %+v", test.expectedError, err) - } - return - } - - if res.Type().String() != "Array" { - t.Errorf("expected type Array, got: %s", res.Type().String()) - } - for i, value := range res.Array() { - if test.expected[i] == nil { - if !value.IsNull() { - t.Errorf("expected nil value, got %+v", value) + + if test.expectedErr != nil { + if !strings.Contains(res.Error().Error(), test.expectedErr.Error()) { + t.Errorf("expected error %s, got %s", test.expectedErr.Error(), err.Error()) } - continue + return } - if value.String() != test.expected[i] { - t.Errorf("expected value %s, got: %s", test.expected[i], value.String()) - } - } - }) - } -} -func Test_HandleDEL(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - command []string - presetValues map[string]string - expectedResponse int - expectToExist map[string]bool - expectedErr error - }{ - { - name: "1. Delete multiple keys", - command: []string{"DEL", "DelKey1", "DelKey2", "DelKey3", "DelKey4", "DelKey5"}, - presetValues: map[string]string{ - "DelKey1": "value1", - "DelKey2": "value2", - "DelKey3": "value3", - "DelKey4": "value4", - }, - expectedResponse: 4, - expectToExist: map[string]bool{ - "DelKey1": false, - "DelKey2": false, - "DelKey3": false, - "DelKey4": false, - "DelKey5": false, - }, - expectedErr: nil, - }, - { - name: "2. Return error when DEL is called with no keys", - command: []string{"DEL"}, - presetValues: nil, - expectedResponse: 0, - expectToExist: nil, - expectedErr: errors.New(constants.WrongArgsResponse), - }, - } + if res.String() != test.expectedResponse { + t.Errorf("expected response %s, got %s", test.expectedResponse, res.String()) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - for k, v := range test.presetValues { - if err = client.WriteArray([]resp.Value{ - resp.StringValue("SET"), - resp.StringValue(k), - resp.StringValue(v), - }); err != nil { + for key, expectedValue := range test.expectedValues { + // Get value from server + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { t.Error(err) } + res, _, err = client.ReadValue() + switch expectedValue.(type) { + default: + t.Error("unexpected type for expectedValue") + case int: + ev, _ := expectedValue.(int) + if res.Integer() != ev { + t.Errorf("expected value %d for key %s, got %d", ev, key, res.Integer()) + } + case float64: + ev, _ := expectedValue.(float64) + if res.Float() != ev { + t.Errorf("expected value %f for key %s, got %f", ev, key, res.Float()) + } + case string: + ev, _ := expectedValue.(string) + if res.String() != ev { + t.Errorf("expected value %s for key %s, got %s", ev, key, res.String()) + } + } + } + }) + } + }) + + t.Run("Test_HandleGET", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + value string + }{ + { + name: "1. String", + key: "GetKey1", + value: "value1", + }, + { + name: "2. Integer", + key: "GetKey2", + value: "10", + }, + { + name: "3. Float", + key: "GetKey3", + value: "3.142", + }, + } + // Test successful Get command + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + func(key, value string) { + // Preset the values + err = client.WriteArray([]resp.Value{resp.StringValue("SET"), resp.StringValue(key), resp.StringValue(value)}) + if err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() if err != nil { t.Error(err) } + if !strings.EqualFold(res.String(), "ok") { t.Errorf("expected preset response to be \"OK\", got %s", res.String()) } - } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + t.Error(err) + } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedErr != nil { - if !strings.Contains(res.Error().Error(), test.expectedErr.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedErr.Error(), res.Error().Error()) + if res.String() != test.value { + t.Errorf("expected value %s, got %s", test.value, res.String()) + } + }(test.key, test.value) + }) + } + + // Test get non-existent key + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue("test4")}); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + if !res.IsNull() { + t.Errorf("expected nil, got: %+v", res) + } + + errorTests := []struct { + name string + command []string + expected string + }{ + { + name: "1. Return error when no GET key is passed", + command: []string{"GET"}, + expected: constants.WrongArgsResponse, + }, + { + name: "2. Return error when too many GET keys are passed", + command: []string{"GET", "GetKey1", "test"}, + expected: constants.WrongArgsResponse, + }, + } + for _, test := range errorTests { + t.Run(test.name, func(t *testing.T) { + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - for key, expected := range test.expectToExist { - if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - exists := !res.IsNull() - if exists != expected { - t.Errorf("expected existence of key %s to be %v, got %v", key, expected, exists) - } - } - }) - } -} - -func Test_HandlePERSIST(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - command []string - presetValues map[string]KeyData - expectedResponse int - expectedValues map[string]KeyData - expectedError error - }{ - { - name: "1. Successfully persist a volatile key", - command: []string{"PERSIST", "PersistKey1"}, - presetValues: map[string]KeyData{ - "PersistKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "PersistKey1": {Value: "value1", ExpireAt: time.Time{}}, - }, - expectedError: nil, - }, - { - name: "2. Return 0 when trying to persist a non-existent key", - command: []string{"PERSIST", "PersistKey2"}, - presetValues: nil, - expectedResponse: 0, - expectedValues: nil, - expectedError: nil, - }, - { - name: "3. Return 0 when trying to persist a non-volatile key", - command: []string{"PERSIST", "PersistKey3"}, - presetValues: map[string]KeyData{ - "PersistKey3": {Value: "value3", ExpireAt: time.Time{}}, - }, - expectedResponse: 0, - expectedValues: map[string]KeyData{ - "PersistKey3": {Value: "value3", ExpireAt: time.Time{}}, - }, - expectedError: nil, - }, - { - name: "4. Command too short", - command: []string{"PERSIST"}, - presetValues: nil, - expectedResponse: 0, - expectedValues: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Command too long", - command: []string{"PERSIST", "PersistKey5", "key6"}, - presetValues: nil, - expectedResponse: 0, - expectedValues: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - for k, v := range test.presetValues { - command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} - if !v.ExpireAt.Equal(time.Time{}) { - command = append(command, []resp.Value{ - resp.StringValue("PX"), - resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), - }...) - } - if err = client.WriteArray(command); err != nil { + if !strings.Contains(res.Error().Error(), test.expected) { + t.Errorf("expected error '%s', got: %s", test.expected, err.Error()) + } + }) + } + }) + + t.Run("Test_HandleMGET", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetKeys []string + presetValues []string + command []string + expected []interface{} + expectedError error + }{ + { + name: "1. MGET multiple existing values", + presetKeys: []string{"MgetKey1", "MgetKey2", "MgetKey3", "MgetKey4"}, + presetValues: []string{"value1", "value2", "value3", "value4"}, + command: []string{"MGET", "MgetKey1", "MgetKey4", "MgetKey2", "MgetKey3", "MgetKey1"}, + expected: []interface{}{"value1", "value4", "value2", "value3", "value1"}, + expectedError: nil, + }, + { + name: "2. MGET multiple values with nil values spliced in", + presetKeys: []string{"MgetKey5", "MgetKey6", "MgetKey7"}, + presetValues: []string{"value5", "value6", "value7"}, + command: []string{"MGET", "MgetKey5", "MgetKey6", "non-existent", "non-existent", "MgetKey7", "non-existent"}, + expected: []interface{}{"value5", "value6", nil, nil, "value7", nil}, + expectedError: nil, + }, + { + name: "3. Return error when MGET is invoked with no keys", + presetKeys: []string{"MgetKey5"}, + presetValues: []string{"value5"}, + command: []string{"MGET"}, + expected: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Set up the values + for i, key := range test.presetKeys { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(test.presetValues[i]), + }); err != nil { t.Error(err) } res, _, err := client.ReadValue() @@ -954,890 +740,1158 @@ func Test_HandlePERSIST(t *testing.T) { t.Error(err) } if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected preset response to be OK, got %s", res.String()) + t.Errorf("expected preset response to be \"OK\", got \"%s\"", res.String()) } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + // Test the command and its results + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - if test.expectedValues == nil { - return - } - - for key, expected := range test.expectedValues { - // Compare the value of the key with what's expected - if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - res, _, err = client.ReadValue() + + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if res.String() != expected.Value.(string) { - t.Errorf("expected value %s, got %s", expected.Value.(string), res.String()) + + if test.expectedError != nil { + // If we expect and error, branch out and check error + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error %+v, got: %+v", test.expectedError, err) + } + return + } + + if res.Type().String() != "Array" { + t.Errorf("expected type Array, got: %s", res.Type().String()) + } + for i, value := range res.Array() { + if test.expected[i] == nil { + if !value.IsNull() { + t.Errorf("expected nil value, got %+v", value) + } + continue + } + if value.String() != test.expected[i] { + t.Errorf("expected value %s, got: %s", test.expected[i], value.String()) + } + } + }) + } + }) + + t.Run("Test_HandleDEL", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + command []string + presetValues map[string]string + expectedResponse int + expectToExist map[string]bool + expectedErr error + }{ + { + name: "1. Delete multiple keys", + command: []string{"DEL", "DelKey1", "DelKey2", "DelKey3", "DelKey4", "DelKey5"}, + presetValues: map[string]string{ + "DelKey1": "value1", + "DelKey2": "value2", + "DelKey3": "value3", + "DelKey4": "value4", + }, + expectedResponse: 4, + expectToExist: map[string]bool{ + "DelKey1": false, + "DelKey2": false, + "DelKey3": false, + "DelKey4": false, + "DelKey5": false, + }, + expectedErr: nil, + }, + { + name: "2. Return error when DEL is called with no keys", + command: []string{"DEL"}, + presetValues: nil, + expectedResponse: 0, + expectToExist: nil, + expectedErr: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + for k, v := range test.presetValues { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue(k), + resp.StringValue(v), + }); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be \"OK\", got %s", res.String()) + } + } } - // Compare the expiry of the key with what's expected - if err = client.WriteArray([]resp.Value{resp.StringValue("PTTL"), resp.StringValue(key)}); err != nil { + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - res, _, err = client.ReadValue() + + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if expected.ExpireAt.Equal(time.Time{}) { - if res.Integer() != -1 { - t.Error("expected key to be persisted, it was not.") + + if test.expectedErr != nil { + if !strings.Contains(res.Error().Error(), test.expectedErr.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedErr.Error(), res.Error().Error()) } - continue - } - if res.Integer() != int(expected.ExpireAt.UnixMilli()) { - t.Errorf("expected expiry %d, got %d", expected.ExpireAt.UnixMilli(), res.Integer()) + return } - } - }) - } -} -func Test_HandleEXPIRETIME(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - command []string - presetValues map[string]KeyData - expectedResponse int - expectedError error - }{ - { - name: "1. Return expire time in seconds", - command: []string{"EXPIRETIME", "ExpireTimeKey1"}, - presetValues: map[string]KeyData{ - "ExpireTimeKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(100 * time.Second)}, - }, - expectedResponse: int(mockClock.Now().Add(100 * time.Second).Unix()), - expectedError: nil, - }, - { - name: "2. Return expire time in milliseconds", - command: []string{"PEXPIRETIME", "ExpireTimeKey2"}, - presetValues: map[string]KeyData{ - "ExpireTimeKey2": {Value: "value2", ExpireAt: mockClock.Now().Add(4096 * time.Millisecond)}, - }, - expectedResponse: int(mockClock.Now().Add(4096 * time.Millisecond).UnixMilli()), - expectedError: nil, - }, - { - name: "3. If the key is non-volatile, return -1", - command: []string{"PEXPIRETIME", "ExpireTimeKey3"}, - presetValues: map[string]KeyData{ - "ExpireTimeKey3": {Value: "value3", ExpireAt: time.Time{}}, - }, - expectedResponse: -1, - expectedError: nil, - }, - { - name: "4. If the key is non-existent return -2", - command: []string{"PEXPIRETIME", "ExpireTimeKey4"}, - presetValues: nil, - expectedResponse: -2, - expectedError: nil, - }, - { - name: "5. Command too short", - command: []string{"PEXPIRETIME"}, - presetValues: nil, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - command: []string{"PEXPIRETIME", "ExpireTimeKey5", "ExpireTimeKey6"}, - presetValues: nil, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - for k, v := range test.presetValues { - command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} - if !v.ExpireAt.Equal(time.Time{}) { - command = append(command, []resp.Value{ - resp.StringValue("PX"), - resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), - }...) - } - if err = client.WriteArray(command); err != nil { + for key, expected := range test.expectToExist { + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected preset response to be OK, got %s", res.String()) + exists := !res.IsNull() + if exists != expected { + t.Errorf("expected existence of key %s to be %v, got %v", key, expected, exists) + } + } + }) + } + }) + + t.Run("Test_HandlePERSIST", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + command []string + presetValues map[string]KeyData + expectedResponse int + expectedValues map[string]KeyData + expectedError error + }{ + { + name: "1. Successfully persist a volatile key", + command: []string{"PERSIST", "PersistKey1"}, + presetValues: map[string]KeyData{ + "PersistKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "PersistKey1": {Value: "value1", ExpireAt: time.Time{}}, + }, + expectedError: nil, + }, + { + name: "2. Return 0 when trying to persist a non-existent key", + command: []string{"PERSIST", "PersistKey2"}, + presetValues: nil, + expectedResponse: 0, + expectedValues: nil, + expectedError: nil, + }, + { + name: "3. Return 0 when trying to persist a non-volatile key", + command: []string{"PERSIST", "PersistKey3"}, + presetValues: map[string]KeyData{ + "PersistKey3": {Value: "value3", ExpireAt: time.Time{}}, + }, + expectedResponse: 0, + expectedValues: map[string]KeyData{ + "PersistKey3": {Value: "value3", ExpireAt: time.Time{}}, + }, + expectedError: nil, + }, + { + name: "4. Command too short", + command: []string{"PERSIST"}, + presetValues: nil, + expectedResponse: 0, + expectedValues: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Command too long", + command: []string{"PERSIST", "PersistKey5", "key6"}, + presetValues: nil, + expectedResponse: 0, + expectedValues: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + for k, v := range test.presetValues { + command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} + if !v.ExpireAt.Equal(time.Time{}) { + command = append(command, []resp.Value{ + resp.StringValue("PX"), + resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), + }...) + } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } } } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - }) - } -} + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } -func Test_HandleTTL(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - command []string - presetValues map[string]KeyData - expectedResponse int - expectedError error - }{ - { - name: "1. Return TTL time in seconds", - command: []string{"TTL", "TTLKey1"}, - presetValues: map[string]KeyData{ - "TTLKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(100 * time.Second)}, - }, - expectedResponse: 100, - expectedError: nil, - }, - { - name: "2. Return TTL time in milliseconds", - command: []string{"PTTL", "TTLKey2"}, - presetValues: map[string]KeyData{ - "TTLKey2": {Value: "value2", ExpireAt: mockClock.Now().Add(4096 * time.Millisecond)}, - }, - expectedResponse: 4096, - expectedError: nil, - }, - { - name: "3. If the key is non-volatile, return -1", - command: []string{"TTL", "TTLKey3"}, - presetValues: map[string]KeyData{ - "TTLKey3": {Value: "value3", ExpireAt: time.Time{}}, - }, - expectedResponse: -1, - expectedError: nil, - }, - { - name: "4. If the key is non-existent return -2", - command: []string{"TTL", "TTLKey4"}, - presetValues: nil, - expectedResponse: -2, - expectedError: nil, - }, - { - name: "5. Command too short", - command: []string{"TTL"}, - presetValues: nil, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - command: []string{"TTL", "TTLKey5", "TTLKey6"}, - presetValues: nil, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if test.expectedValues == nil { + return + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - for k, v := range test.presetValues { - command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} - if !v.ExpireAt.Equal(time.Time{}) { - command = append(command, []resp.Value{ - resp.StringValue("PX"), - resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), - }...) - } - if err = client.WriteArray(command); err != nil { + for key, expected := range test.expectedValues { + // Compare the value of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected preset response to be OK, got %s", res.String()) + if res.String() != expected.Value.(string) { + t.Errorf("expected value %s, got %s", expected.Value.(string), res.String()) } - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - }) - } -} - -func Test_HandleEXPIRE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - command []string - presetValues map[string]KeyData - expectedResponse int - expectedValues map[string]KeyData - expectedError error - }{ - { - name: "1. Set new expire by seconds", - command: []string{"EXPIRE", "ExpireKey1", "100"}, - presetValues: map[string]KeyData{ - "ExpireKey1": {Value: "value1", ExpireAt: time.Time{}}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(100 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "2. Set new expire by milliseconds", - command: []string{"PEXPIRE", "ExpireKey2", "1000"}, - presetValues: map[string]KeyData{ - "ExpireKey2": {Value: "value2", ExpireAt: time.Time{}}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireKey2": {Value: "value2", ExpireAt: mockClock.Now().Add(1000 * time.Millisecond)}, - }, - expectedError: nil, - }, - { - name: "3. Set new expire only when key does not have an expiry time with NX flag", - command: []string{"EXPIRE", "ExpireKey3", "1000", "NX"}, - presetValues: map[string]KeyData{ - "ExpireKey3": {Value: "value3", ExpireAt: time.Time{}}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireKey3": {Value: "value3", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "4. Return 0, when NX flag is provided and key already has an expiry time", - command: []string{"EXPIRE", "ExpireKey4", "1000", "NX"}, - presetValues: map[string]KeyData{ - "ExpireKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedResponse: 0, - expectedValues: map[string]KeyData{ - "ExpireKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "5. Set new expire time from now key only when the key already has an expiry time with XX flag", - command: []string{"EXPIRE", "ExpireKey5", "1000", "XX"}, - presetValues: map[string]KeyData{ - "ExpireKey5": {Value: "value5", ExpireAt: mockClock.Now().Add(30 * time.Second)}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireKey5": {Value: "value5", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "6. Return 0 when key does not have an expiry and the XX flag is provided", - command: []string{"EXPIRE", "ExpireKey6", "1000", "XX"}, - presetValues: map[string]KeyData{ - "ExpireKey6": {Value: "value6", ExpireAt: time.Time{}}, - }, - expectedResponse: 0, - expectedValues: map[string]KeyData{ - "ExpireKey6": {Value: "value6", ExpireAt: time.Time{}}, - }, - expectedError: nil, - }, - { - name: "7. Set expiry time when the provided time is after the current expiry time when GT flag is provided", - command: []string{"EXPIRE", "ExpireKey7", "1000", "GT"}, - presetValues: map[string]KeyData{ - "ExpireKey7": {Value: "value7", ExpireAt: mockClock.Now().Add(30 * time.Second)}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireKey7": {Value: "value7", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "8. Return 0 when GT flag is passed and current expiry time is greater than provided time", - command: []string{"EXPIRE", "ExpireKey8", "1000", "GT"}, - presetValues: map[string]KeyData{ - "ExpireKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, - }, - expectedResponse: 0, - expectedValues: map[string]KeyData{ - "ExpireKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "9. Return 0 when GT flag is passed and key does not have an expiry time", - command: []string{"EXPIRE", "ExpireKey9", "1000", "GT"}, - presetValues: map[string]KeyData{ - "ExpireKey9": {Value: "value9", ExpireAt: time.Time{}}, - }, - expectedResponse: 0, - expectedValues: map[string]KeyData{ - "ExpireKey9": {Value: "value9", ExpireAt: time.Time{}}, - }, - expectedError: nil, - }, - { - name: "10. Set expiry time when the provided time is before the current expiry time when LT flag is provided", - command: []string{"EXPIRE", "ExpireKey10", "1000", "LT"}, - presetValues: map[string]KeyData{ - "ExpireKey10": {Value: "value10", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireKey10": {Value: "value10", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "11. Return 0 when LT flag is passed and current expiry time is less than provided time", - command: []string{"EXPIRE", "ExpireKey11", "5000", "LT"}, - presetValues: map[string]KeyData{ - "ExpireKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, - }, - expectedResponse: 0, - expectedValues: map[string]KeyData{ - "ExpireKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "12. Return 0 when LT flag is passed and key does not have an expiry time", - command: []string{"EXPIRE", "ExpireKey12", "1000", "LT"}, - presetValues: map[string]KeyData{ - "ExpireKey12": {Value: "value12", ExpireAt: time.Time{}}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireKey12": {Value: "value12", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "13. Return error when unknown flag is passed", - command: []string{"EXPIRE", "ExpireKey13", "1000", "UNKNOWN"}, - presetValues: map[string]KeyData{ - "ExpireKey13": {Value: "value13", ExpireAt: time.Time{}}, - }, - expectedResponse: 0, - expectedValues: nil, - expectedError: errors.New("unknown option UNKNOWN"), - }, - { - name: "14. Return error when expire time is not a valid integer", - command: []string{"EXPIRE", "ExpireKey14", "expire"}, - presetValues: nil, - expectedResponse: 0, - expectedValues: nil, - expectedError: errors.New("expire time must be integer"), - }, - { - name: "15. Command too short", - command: []string{"EXPIRE"}, - presetValues: nil, - expectedResponse: 0, - expectedValues: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "16. Command too long", - command: []string{"EXPIRE", "ExpireKey16", "10", "NX", "GT"}, - presetValues: nil, - expectedResponse: 0, - expectedValues: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - for k, v := range test.presetValues { - command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} - if !v.ExpireAt.Equal(time.Time{}) { - command = append(command, []resp.Value{ - resp.StringValue("PX"), - resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), - }...) - } - if err = client.WriteArray(command); err != nil { + // Compare the expiry of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("PTTL"), resp.StringValue(key)}); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected preset response to be OK, got %s", res.String()) + if expected.ExpireAt.Equal(time.Time{}) { + if res.Integer() != -1 { + t.Error("expected key to be persisted, it was not.") + } + continue + } + if res.Integer() != int(expected.ExpireAt.UnixMilli()) { + t.Errorf("expected expiry %d, got %d", expected.ExpireAt.UnixMilli(), res.Integer()) + } + } + }) + } + }) + + t.Run("Test_HandleEXPIRETIME", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + command []string + presetValues map[string]KeyData + expectedResponse int + expectedError error + }{ + { + name: "1. Return expire time in seconds", + command: []string{"EXPIRETIME", "ExpireTimeKey1"}, + presetValues: map[string]KeyData{ + "ExpireTimeKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(100 * time.Second)}, + }, + expectedResponse: int(mockClock.Now().Add(100 * time.Second).Unix()), + expectedError: nil, + }, + { + name: "2. Return expire time in milliseconds", + command: []string{"PEXPIRETIME", "ExpireTimeKey2"}, + presetValues: map[string]KeyData{ + "ExpireTimeKey2": {Value: "value2", ExpireAt: mockClock.Now().Add(4096 * time.Millisecond)}, + }, + expectedResponse: int(mockClock.Now().Add(4096 * time.Millisecond).UnixMilli()), + expectedError: nil, + }, + { + name: "3. If the key is non-volatile, return -1", + command: []string{"PEXPIRETIME", "ExpireTimeKey3"}, + presetValues: map[string]KeyData{ + "ExpireTimeKey3": {Value: "value3", ExpireAt: time.Time{}}, + }, + expectedResponse: -1, + expectedError: nil, + }, + { + name: "4. If the key is non-existent return -2", + command: []string{"PEXPIRETIME", "ExpireTimeKey4"}, + presetValues: nil, + expectedResponse: -2, + expectedError: nil, + }, + { + name: "5. Command too short", + command: []string{"PEXPIRETIME"}, + presetValues: nil, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + command: []string{"PEXPIRETIME", "ExpireTimeKey5", "ExpireTimeKey6"}, + presetValues: nil, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + for k, v := range test.presetValues { + command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} + if !v.ExpireAt.Equal(time.Time{}) { + command = append(command, []resp.Value{ + resp.StringValue("PX"), + resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), + }...) + } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } } } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleTTL", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + command []string + presetValues map[string]KeyData + expectedResponse int + expectedError error + }{ + { + name: "1. Return TTL time in seconds", + command: []string{"TTL", "TTLKey1"}, + presetValues: map[string]KeyData{ + "TTLKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(100 * time.Second)}, + }, + expectedResponse: 100, + expectedError: nil, + }, + { + name: "2. Return TTL time in milliseconds", + command: []string{"PTTL", "TTLKey2"}, + presetValues: map[string]KeyData{ + "TTLKey2": {Value: "value2", ExpireAt: mockClock.Now().Add(4096 * time.Millisecond)}, + }, + expectedResponse: 4096, + expectedError: nil, + }, + { + name: "3. If the key is non-volatile, return -1", + command: []string{"TTL", "TTLKey3"}, + presetValues: map[string]KeyData{ + "TTLKey3": {Value: "value3", ExpireAt: time.Time{}}, + }, + expectedResponse: -1, + expectedError: nil, + }, + { + name: "4. If the key is non-existent return -2", + command: []string{"TTL", "TTLKey4"}, + presetValues: nil, + expectedResponse: -2, + expectedError: nil, + }, + { + name: "5. Command too short", + command: []string{"TTL"}, + presetValues: nil, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + command: []string{"TTL", "TTLKey5", "TTLKey6"}, + presetValues: nil, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + for k, v := range test.presetValues { + command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} + if !v.ExpireAt.Equal(time.Time{}) { + command = append(command, []resp.Value{ + resp.StringValue("PX"), + resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), + }...) + } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } + } + } - if test.expectedValues == nil { - return - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - for key, expected := range test.expectedValues { - // Compare the value of the key with what's expected - if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - res, _, err = client.ReadValue() + + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if res.String() != expected.Value.(string) { - t.Errorf("expected value %s, got %s", expected.Value.(string), res.String()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - // Compare the expiry of the key with what's expected - if err = client.WriteArray([]resp.Value{resp.StringValue("PTTL"), resp.StringValue(key)}); err != nil { + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleEXPIRE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + command []string + presetValues map[string]KeyData + expectedResponse int + expectedValues map[string]KeyData + expectedError error + }{ + { + name: "1. Set new expire by seconds", + command: []string{"EXPIRE", "ExpireKey1", "100"}, + presetValues: map[string]KeyData{ + "ExpireKey1": {Value: "value1", ExpireAt: time.Time{}}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(100 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "2. Set new expire by milliseconds", + command: []string{"PEXPIRE", "ExpireKey2", "1000"}, + presetValues: map[string]KeyData{ + "ExpireKey2": {Value: "value2", ExpireAt: time.Time{}}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireKey2": {Value: "value2", ExpireAt: mockClock.Now().Add(1000 * time.Millisecond)}, + }, + expectedError: nil, + }, + { + name: "3. Set new expire only when key does not have an expiry time with NX flag", + command: []string{"EXPIRE", "ExpireKey3", "1000", "NX"}, + presetValues: map[string]KeyData{ + "ExpireKey3": {Value: "value3", ExpireAt: time.Time{}}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireKey3": {Value: "value3", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "4. Return 0, when NX flag is provided and key already has an expiry time", + command: []string{"EXPIRE", "ExpireKey4", "1000", "NX"}, + presetValues: map[string]KeyData{ + "ExpireKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedResponse: 0, + expectedValues: map[string]KeyData{ + "ExpireKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "5. Set new expire time from now key only when the key already has an expiry time with XX flag", + command: []string{"EXPIRE", "ExpireKey5", "1000", "XX"}, + presetValues: map[string]KeyData{ + "ExpireKey5": {Value: "value5", ExpireAt: mockClock.Now().Add(30 * time.Second)}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireKey5": {Value: "value5", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "6. Return 0 when key does not have an expiry and the XX flag is provided", + command: []string{"EXPIRE", "ExpireKey6", "1000", "XX"}, + presetValues: map[string]KeyData{ + "ExpireKey6": {Value: "value6", ExpireAt: time.Time{}}, + }, + expectedResponse: 0, + expectedValues: map[string]KeyData{ + "ExpireKey6": {Value: "value6", ExpireAt: time.Time{}}, + }, + expectedError: nil, + }, + { + name: "7. Set expiry time when the provided time is after the current expiry time when GT flag is provided", + command: []string{"EXPIRE", "ExpireKey7", "1000", "GT"}, + presetValues: map[string]KeyData{ + "ExpireKey7": {Value: "value7", ExpireAt: mockClock.Now().Add(30 * time.Second)}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireKey7": {Value: "value7", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "8. Return 0 when GT flag is passed and current expiry time is greater than provided time", + command: []string{"EXPIRE", "ExpireKey8", "1000", "GT"}, + presetValues: map[string]KeyData{ + "ExpireKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, + }, + expectedResponse: 0, + expectedValues: map[string]KeyData{ + "ExpireKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "9. Return 0 when GT flag is passed and key does not have an expiry time", + command: []string{"EXPIRE", "ExpireKey9", "1000", "GT"}, + presetValues: map[string]KeyData{ + "ExpireKey9": {Value: "value9", ExpireAt: time.Time{}}, + }, + expectedResponse: 0, + expectedValues: map[string]KeyData{ + "ExpireKey9": {Value: "value9", ExpireAt: time.Time{}}, + }, + expectedError: nil, + }, + { + name: "10. Set expiry time when the provided time is before the current expiry time when LT flag is provided", + command: []string{"EXPIRE", "ExpireKey10", "1000", "LT"}, + presetValues: map[string]KeyData{ + "ExpireKey10": {Value: "value10", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireKey10": {Value: "value10", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "11. Return 0 when LT flag is passed and current expiry time is less than provided time", + command: []string{"EXPIRE", "ExpireKey11", "5000", "LT"}, + presetValues: map[string]KeyData{ + "ExpireKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, + }, + expectedResponse: 0, + expectedValues: map[string]KeyData{ + "ExpireKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "12. Return 0 when LT flag is passed and key does not have an expiry time", + command: []string{"EXPIRE", "ExpireKey12", "1000", "LT"}, + presetValues: map[string]KeyData{ + "ExpireKey12": {Value: "value12", ExpireAt: time.Time{}}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireKey12": {Value: "value12", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "13. Return error when unknown flag is passed", + command: []string{"EXPIRE", "ExpireKey13", "1000", "UNKNOWN"}, + presetValues: map[string]KeyData{ + "ExpireKey13": {Value: "value13", ExpireAt: time.Time{}}, + }, + expectedResponse: 0, + expectedValues: nil, + expectedError: errors.New("unknown option UNKNOWN"), + }, + { + name: "14. Return error when expire time is not a valid integer", + command: []string{"EXPIRE", "ExpireKey14", "expire"}, + presetValues: nil, + expectedResponse: 0, + expectedValues: nil, + expectedError: errors.New("expire time must be integer"), + }, + { + name: "15. Command too short", + command: []string{"EXPIRE"}, + presetValues: nil, + expectedResponse: 0, + expectedValues: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "16. Command too long", + command: []string{"EXPIRE", "ExpireKey16", "10", "NX", "GT"}, + presetValues: nil, + expectedResponse: 0, + expectedValues: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + for k, v := range test.presetValues { + command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} + if !v.ExpireAt.Equal(time.Time{}) { + command = append(command, []resp.Value{ + resp.StringValue("PX"), + resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), + }...) + } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } - res, _, err = client.ReadValue() + + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if expected.ExpireAt.Equal(time.Time{}) { - if res.Integer() != -1 { - t.Error("expected key to be persisted, it was not.") + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } - continue + return } - if res.Integer() != int(expected.ExpireAt.Sub(mockClock.Now()).Milliseconds()) { - t.Errorf("expected expiry %d, got %d", expected.ExpireAt.Sub(mockClock.Now()).Milliseconds(), res.Integer()) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } - } - }) - } -} -func Test_HandleEXPIREAT(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - command []string - presetValues map[string]KeyData - expectedResponse int - expectedValues map[string]KeyData - expectedError error - }{ - { - name: "1. Set new expire by unix seconds", - command: []string{"EXPIREAT", "ExpireAtKey1", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix())}, - presetValues: map[string]KeyData{ - "ExpireAtKey1": {Value: "value1", ExpireAt: time.Time{}}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireAtKey1": {Value: "value1", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, - }, - expectedError: nil, - }, - { - name: "2. Set new expire by milliseconds", - command: []string{"PEXPIREAT", "ExpireAtKey2", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).UnixMilli())}, - presetValues: map[string]KeyData{ - "ExpireAtKey2": {Value: "value2", ExpireAt: time.Time{}}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireAtKey2": {Value: "value2", ExpireAt: time.UnixMilli(mockClock.Now().Add(1000 * time.Second).UnixMilli())}, - }, - expectedError: nil, - }, - { - name: "3. Set new expire only when key does not have an expiry time with NX flag", - command: []string{"EXPIREAT", "ExpireAtKey3", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "NX"}, - presetValues: map[string]KeyData{ - "ExpireAtKey3": {Value: "value3", ExpireAt: time.Time{}}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireAtKey3": {Value: "value3", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, - }, - expectedError: nil, - }, - { - name: "4. Return 0, when NX flag is provided and key already has an expiry time", - command: []string{"EXPIREAT", "ExpireAtKey4", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "NX"}, - presetValues: map[string]KeyData{ - "ExpireAtKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedResponse: 0, - expectedValues: map[string]KeyData{ - "ExpireAtKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "5. Set new expire time from now key only when the key already has an expiry time with XX flag", - command: []string{ - "EXPIREAT", "ExpireAtKey5", - fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "XX", - }, - presetValues: map[string]KeyData{ - "ExpireAtKey5": {Value: "value5", ExpireAt: mockClock.Now().Add(30 * time.Second)}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireAtKey5": {Value: "value5", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, - }, - expectedError: nil, - }, - { - name: "6. Return 0 when key does not have an expiry and the XX flag is provided", - command: []string{ - "EXPIREAT", "ExpireAtKey6", - fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "XX", - }, - presetValues: map[string]KeyData{ - "ExpireAtKey6": {Value: "value6", ExpireAt: time.Time{}}, - }, - expectedResponse: 0, - expectedValues: map[string]KeyData{ - "ExpireAtKey6": {Value: "value6", ExpireAt: time.Time{}}, - }, - expectedError: nil, - }, - { - name: "7. Set expiry time when the provided time is after the current expiry time when GT flag is provided", - command: []string{ - "EXPIREAT", "ExpireAtKey7", - fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "GT", - }, - presetValues: map[string]KeyData{ - "ExpireAtKey7": {Value: "value7", ExpireAt: mockClock.Now().Add(30 * time.Second)}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireAtKey7": {Value: "value7", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, - }, - expectedError: nil, - }, - { - name: "8. Return 0 when GT flag is passed and current expiry time is greater than provided time", - command: []string{ - "EXPIREAT", "ExpireAtKey8", - fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "GT", - }, - presetValues: map[string]KeyData{ - "ExpireAtKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, - }, - expectedResponse: 0, - expectedValues: map[string]KeyData{ - "ExpireAtKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "9. Return 0 when GT flag is passed and key does not have an expiry time", - command: []string{ - "EXPIREAT", "ExpireAtKey9", - fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "GT", - }, - presetValues: map[string]KeyData{ - "ExpireAtKey9": {Value: "value9", ExpireAt: time.Time{}}, - }, - expectedResponse: 0, - expectedValues: map[string]KeyData{ - "ExpireAtKey9": {Value: "value9", ExpireAt: time.Time{}}, - }, - expectedError: nil, - }, - { - name: "10. Set expiry time when the provided time is before the current expiry time when LT flag is provided", - command: []string{ - "EXPIREAT", "ExpireAtKey10", - fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "LT", - }, - presetValues: map[string]KeyData{ - "ExpireAtKey10": {Value: "value10", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireAtKey10": {Value: "value10", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, - }, - expectedError: nil, - }, - { - name: "11. Return 0 when LT flag is passed and current expiry time is less than provided time", - command: []string{ - "EXPIREAT", "ExpireAtKey11", - fmt.Sprintf("%d", mockClock.Now().Add(3000*time.Second).Unix()), "LT", - }, - presetValues: map[string]KeyData{ - "ExpireAtKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedResponse: 0, - expectedValues: map[string]KeyData{ - "ExpireAtKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, - }, - expectedError: nil, - }, - { - name: "12. Return 0 when LT flag is passed and key does not have an expiry time", - command: []string{ - "EXPIREAT", "ExpireAtKey12", - fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "LT", - }, - presetValues: map[string]KeyData{ - "ExpireAtKey12": {Value: "value12", ExpireAt: time.Time{}}, - }, - expectedResponse: 1, - expectedValues: map[string]KeyData{ - "ExpireAtKey12": {Value: "value12", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, - }, - expectedError: nil, - }, - { - name: "13. Return error when unknown flag is passed", - command: []string{"EXPIREAT", "ExpireAtKey13", "1000", "UNKNOWN"}, - presetValues: map[string]KeyData{ - "ExpireAtKey13": {Value: "value13", ExpireAt: time.Time{}}, - }, - expectedResponse: 0, - expectedValues: nil, - expectedError: errors.New("unknown option UNKNOWN"), - }, - { - name: "14. Return error when expire time is not a valid integer", - command: []string{"EXPIREAT", "ExpireAtKey14", "expire"}, - presetValues: nil, - expectedResponse: 0, - expectedValues: nil, - expectedError: errors.New("expire time must be integer"), - }, - { - name: "15. Command too short", - command: []string{"EXPIREAT"}, - presetValues: nil, - expectedResponse: 0, - expectedValues: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "16. Command too long", - command: []string{"EXPIREAT", "ExpireAtKey16", "10", "NX", "GT"}, - presetValues: nil, - expectedResponse: 0, - expectedValues: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if test.expectedValues == nil { + return + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - for k, v := range test.presetValues { - command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} - if !v.ExpireAt.Equal(time.Time{}) { - command = append(command, []resp.Value{ - resp.StringValue("PX"), - resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), - }...) + for key, expected := range test.expectedValues { + // Compare the value of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + t.Error(err) } - if err = client.WriteArray(command); err != nil { + res, _, err = client.ReadValue() + if err != nil { t.Error(err) } - res, _, err := client.ReadValue() + if res.String() != expected.Value.(string) { + t.Errorf("expected value %s, got %s", expected.Value.(string), res.String()) + } + // Compare the expiry of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("PTTL"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected preset response to be OK, got %s", res.String()) + if expected.ExpireAt.Equal(time.Time{}) { + if res.Integer() != -1 { + t.Error("expected key to be persisted, it was not.") + } + continue + } + if res.Integer() != int(expected.ExpireAt.Sub(mockClock.Now()).Milliseconds()) { + t.Errorf("expected expiry %d, got %d", expected.ExpireAt.Sub(mockClock.Now()).Milliseconds(), res.Integer()) } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + }) + } + }) + + t.Run("Test_HandleEXPIREAT", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + command []string + presetValues map[string]KeyData + expectedResponse int + expectedValues map[string]KeyData + expectedError error + }{ + { + name: "1. Set new expire by unix seconds", + command: []string{"EXPIREAT", "ExpireAtKey1", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix())}, + presetValues: map[string]KeyData{ + "ExpireAtKey1": {Value: "value1", ExpireAt: time.Time{}}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireAtKey1": {Value: "value1", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, + }, + expectedError: nil, + }, + { + name: "2. Set new expire by milliseconds", + command: []string{"PEXPIREAT", "ExpireAtKey2", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).UnixMilli())}, + presetValues: map[string]KeyData{ + "ExpireAtKey2": {Value: "value2", ExpireAt: time.Time{}}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireAtKey2": {Value: "value2", ExpireAt: time.UnixMilli(mockClock.Now().Add(1000 * time.Second).UnixMilli())}, + }, + expectedError: nil, + }, + { + name: "3. Set new expire only when key does not have an expiry time with NX flag", + command: []string{"EXPIREAT", "ExpireAtKey3", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "NX"}, + presetValues: map[string]KeyData{ + "ExpireAtKey3": {Value: "value3", ExpireAt: time.Time{}}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireAtKey3": {Value: "value3", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, + }, + expectedError: nil, + }, + { + name: "4. Return 0, when NX flag is provided and key already has an expiry time", + command: []string{"EXPIREAT", "ExpireAtKey4", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "NX"}, + presetValues: map[string]KeyData{ + "ExpireAtKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedResponse: 0, + expectedValues: map[string]KeyData{ + "ExpireAtKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "5. Set new expire time from now key only when the key already has an expiry time with XX flag", + command: []string{ + "EXPIREAT", "ExpireAtKey5", + fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "XX", + }, + presetValues: map[string]KeyData{ + "ExpireAtKey5": {Value: "value5", ExpireAt: mockClock.Now().Add(30 * time.Second)}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireAtKey5": {Value: "value5", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, + }, + expectedError: nil, + }, + { + name: "6. Return 0 when key does not have an expiry and the XX flag is provided", + command: []string{ + "EXPIREAT", "ExpireAtKey6", + fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "XX", + }, + presetValues: map[string]KeyData{ + "ExpireAtKey6": {Value: "value6", ExpireAt: time.Time{}}, + }, + expectedResponse: 0, + expectedValues: map[string]KeyData{ + "ExpireAtKey6": {Value: "value6", ExpireAt: time.Time{}}, + }, + expectedError: nil, + }, + { + name: "7. Set expiry time when the provided time is after the current expiry time when GT flag is provided", + command: []string{ + "EXPIREAT", "ExpireAtKey7", + fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "GT", + }, + presetValues: map[string]KeyData{ + "ExpireAtKey7": {Value: "value7", ExpireAt: mockClock.Now().Add(30 * time.Second)}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireAtKey7": {Value: "value7", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, + }, + expectedError: nil, + }, + { + name: "8. Return 0 when GT flag is passed and current expiry time is greater than provided time", + command: []string{ + "EXPIREAT", "ExpireAtKey8", + fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "GT", + }, + presetValues: map[string]KeyData{ + "ExpireAtKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, + }, + expectedResponse: 0, + expectedValues: map[string]KeyData{ + "ExpireAtKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "9. Return 0 when GT flag is passed and key does not have an expiry time", + command: []string{ + "EXPIREAT", "ExpireAtKey9", + fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "GT", + }, + presetValues: map[string]KeyData{ + "ExpireAtKey9": {Value: "value9", ExpireAt: time.Time{}}, + }, + expectedResponse: 0, + expectedValues: map[string]KeyData{ + "ExpireAtKey9": {Value: "value9", ExpireAt: time.Time{}}, + }, + expectedError: nil, + }, + { + name: "10. Set expiry time when the provided time is before the current expiry time when LT flag is provided", + command: []string{ + "EXPIREAT", "ExpireAtKey10", + fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "LT", + }, + presetValues: map[string]KeyData{ + "ExpireAtKey10": {Value: "value10", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireAtKey10": {Value: "value10", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, + }, + expectedError: nil, + }, + { + name: "11. Return 0 when LT flag is passed and current expiry time is less than provided time", + command: []string{ + "EXPIREAT", "ExpireAtKey11", + fmt.Sprintf("%d", mockClock.Now().Add(3000*time.Second).Unix()), "LT", + }, + presetValues: map[string]KeyData{ + "ExpireAtKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedResponse: 0, + expectedValues: map[string]KeyData{ + "ExpireAtKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, + }, + expectedError: nil, + }, + { + name: "12. Return 0 when LT flag is passed and key does not have an expiry time", + command: []string{ + "EXPIREAT", "ExpireAtKey12", + fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "LT", + }, + presetValues: map[string]KeyData{ + "ExpireAtKey12": {Value: "value12", ExpireAt: time.Time{}}, + }, + expectedResponse: 1, + expectedValues: map[string]KeyData{ + "ExpireAtKey12": {Value: "value12", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)}, + }, + expectedError: nil, + }, + { + name: "13. Return error when unknown flag is passed", + command: []string{"EXPIREAT", "ExpireAtKey13", "1000", "UNKNOWN"}, + presetValues: map[string]KeyData{ + "ExpireAtKey13": {Value: "value13", ExpireAt: time.Time{}}, + }, + expectedResponse: 0, + expectedValues: nil, + expectedError: errors.New("unknown option UNKNOWN"), + }, + { + name: "14. Return error when expire time is not a valid integer", + command: []string{"EXPIREAT", "ExpireAtKey14", "expire"}, + presetValues: nil, + expectedResponse: 0, + expectedValues: nil, + expectedError: errors.New("expire time must be integer"), + }, + { + name: "15. Command too short", + command: []string{"EXPIREAT"}, + presetValues: nil, + expectedResponse: 0, + expectedValues: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "16. Command too long", + command: []string{"EXPIREAT", "ExpireAtKey16", "10", "NX", "GT"}, + presetValues: nil, + expectedResponse: 0, + expectedValues: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + for k, v := range test.presetValues { + command := []resp.Value{resp.StringValue("SET"), resp.StringValue(k), resp.StringValue(v.Value.(string))} + if !v.ExpireAt.Equal(time.Time{}) { + command = append(command, []resp.Value{ + resp.StringValue("PX"), + resp.StringValue(fmt.Sprintf("%d", v.ExpireAt.Sub(mockClock.Now()).Milliseconds())), + }...) + } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } + } } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - - if test.expectedValues == nil { - return - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - for key, expected := range test.expectedValues { - // Compare the value of the key with what's expected - if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - res, _, err = client.ReadValue() + + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if res.String() != expected.Value.(string) { - t.Errorf("expected value %s, got %s", expected.Value.(string), res.String()) + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - // Compare the expiry of the key with what's expected - if err = client.WriteArray([]resp.Value{resp.StringValue("PTTL"), resp.StringValue(key)}); err != nil { - t.Error(err) + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) + + if test.expectedValues == nil { + return } - if expected.ExpireAt.Equal(time.Time{}) { - if res.Integer() != -1 { - t.Error("expected key to be persisted, it was not.") + + for key, expected := range test.expectedValues { + // Compare the value of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if res.String() != expected.Value.(string) { + t.Errorf("expected value %s, got %s", expected.Value.(string), res.String()) + } + // Compare the expiry of the key with what's expected + if err = client.WriteArray([]resp.Value{resp.StringValue("PTTL"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if expected.ExpireAt.Equal(time.Time{}) { + if res.Integer() != -1 { + t.Error("expected key to be persisted, it was not.") + } + continue + } + if res.Integer() != int(expected.ExpireAt.Sub(mockClock.Now()).Milliseconds()) { + t.Errorf("expected expiry %d, got %d", expected.ExpireAt.Sub(mockClock.Now()).Milliseconds(), res.Integer()) } - continue - } - if res.Integer() != int(expected.ExpireAt.Sub(mockClock.Now()).Milliseconds()) { - t.Errorf("expected expiry %d, got %d", expected.ExpireAt.Sub(mockClock.Now()).Milliseconds(), res.Integer()) } - } - }) - } + }) + } + }) + } diff --git a/internal/modules/hash/commands_test.go b/internal/modules/hash/commands_test.go index 09ebc707..0cc27f64 100644 --- a/internal/modules/hash/commands_test.go +++ b/internal/modules/hash/commands_test.go @@ -16,157 +16,182 @@ package hash_test import ( "errors" - "fmt" "github.com/echovault/echovault/echovault" "github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/constants" "github.com/tidwall/resp" - "net" "slices" "strconv" "strings" - "sync" "testing" ) -var mockServer *echovault.EchoVault -var addr = "localhost" -var port int +func Test_Hash(t *testing.T) { + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } -func init() { - port, _ = internal.GetFreePort() - mockServer, _ = echovault.NewEchoVault( + mockServer, err := echovault.NewEchoVault( echovault.WithConfig(config.Config{ - BindAddr: addr, + BindAddr: "localhost", Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() - }() - wg.Wait() -} - -func Test_HandleHSET(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) if err != nil { t.Error(err) - } - client := resp.NewConn(conn) - - // Tests for both HSet and HSetNX - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse int // Change count - expectedValue map[string]string - expectedError error - }{ - { - name: "1. HSETNX set field on non-existent hash map", - key: "HsetKey1", - presetValue: nil, - command: []string{"HSETNX", "HsetKey1", "field1", "value1"}, - expectedResponse: 1, - expectedValue: map[string]string{"field1": "value1"}, - expectedError: nil, - }, - { - name: "2. HSETNX set field on existing hash map", - key: "HsetKey2", - presetValue: map[string]string{"field1": "value1"}, - command: []string{"HSETNX", "HsetKey2", "field2", "value2"}, - expectedResponse: 1, - expectedValue: map[string]string{"field1": "value1", "field2": "value2"}, - expectedError: nil, - }, - { - name: "3. HSETNX skips operation when setting on existing field", - key: "HsetKey3", - presetValue: map[string]string{"field1": "value1"}, - command: []string{"HSETNX", "HsetKey3", "field1", "value1-new"}, - expectedResponse: 0, - expectedValue: map[string]string{"field1": "value1"}, - expectedError: nil, - }, - { - name: "4. Regular HSET command on non-existent hash map", - key: "HsetKey4", - presetValue: nil, - command: []string{"HSET", "HsetKey4", "field1", "value1", "field2", "value2"}, - expectedResponse: 2, - expectedValue: map[string]string{"field1": "value1", "field2": "value2"}, - expectedError: nil, - }, - { - name: "5. Regular HSET update on existing hash map", - key: "HsetKey5", - presetValue: map[string]string{"field1": "value1", "field2": "value2"}, - command: []string{"HSET", "HsetKey5", "field1", "value1-new", "field2", "value2-ne2", "field3", "value3"}, - expectedResponse: 3, - expectedValue: map[string]string{"field1": "value1-new", "field2": "value2-ne2", "field3": "value3"}, - expectedError: nil, - }, - { - name: "6. HSET overwrites when the target key is not a map", - key: "HsetKey6", - presetValue: "Default preset value", - command: []string{"HSET", "HsetKey6", "field1", "value1"}, - expectedResponse: 1, - expectedValue: map[string]string{"field1": "value1"}, - expectedError: nil, - }, - { - name: "7. HSET returns error when there's a mismatch in key/values", - key: "HsetKey7", - presetValue: nil, - command: []string{"HSET", "HsetKey7", "field1", "value1", "field2"}, - expectedResponse: 0, - expectedValue: map[string]string{}, - expectedError: errors.New("each field must have a corresponding value"), - }, - { - name: "8. Command too short", - key: "HsetKey8", - presetValue: nil, - command: []string{"HSET", "field1"}, - expectedResponse: 0, - expectedValue: map[string]string{}, - expectedError: errors.New(constants.WrongArgsResponse), - }, + return } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + go func() { + mockServer.Start() + }() - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + t.Cleanup(func() { + mockServer.ShutDown() + }) + + t.Run("Test_HandleHSET", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + // Tests for both HSet and HSetNX + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse int // Change count + expectedValue map[string]string + expectedError error + }{ + { + name: "1. HSETNX set field on non-existent hash map", + key: "HsetKey1", + presetValue: nil, + command: []string{"HSETNX", "HsetKey1", "field1", "value1"}, + expectedResponse: 1, + expectedValue: map[string]string{"field1": "value1"}, + expectedError: nil, + }, + { + name: "2. HSETNX set field on existing hash map", + key: "HsetKey2", + presetValue: map[string]string{"field1": "value1"}, + command: []string{"HSETNX", "HsetKey2", "field2", "value2"}, + expectedResponse: 1, + expectedValue: map[string]string{"field1": "value1", "field2": "value2"}, + expectedError: nil, + }, + { + name: "3. HSETNX skips operation when setting on existing field", + key: "HsetKey3", + presetValue: map[string]string{"field1": "value1"}, + command: []string{"HSETNX", "HsetKey3", "field1", "value1-new"}, + expectedResponse: 0, + expectedValue: map[string]string{"field1": "value1"}, + expectedError: nil, + }, + { + name: "4. Regular HSET command on non-existent hash map", + key: "HsetKey4", + presetValue: nil, + command: []string{"HSET", "HsetKey4", "field1", "value1", "field2", "value2"}, + expectedResponse: 2, + expectedValue: map[string]string{"field1": "value1", "field2": "value2"}, + expectedError: nil, + }, + { + name: "5. Regular HSET update on existing hash map", + key: "HsetKey5", + presetValue: map[string]string{"field1": "value1", "field2": "value2"}, + command: []string{"HSET", "HsetKey5", "field1", "value1-new", "field2", "value2-ne2", "field3", "value3"}, + expectedResponse: 3, + expectedValue: map[string]string{"field1": "value1-new", "field2": "value2-ne2", "field3": "value3"}, + expectedError: nil, + }, + { + name: "6. HSET overwrites when the target key is not a map", + key: "HsetKey6", + presetValue: "Default preset value", + command: []string{"HSET", "HsetKey6", "field1", "value1"}, + expectedResponse: 1, + expectedValue: map[string]string{"field1": "value1"}, + expectedError: nil, + }, + { + name: "7. HSET returns error when there's a mismatch in key/values", + key: "HsetKey7", + presetValue: nil, + command: []string{"HSET", "HsetKey7", "field1", "value1", "field2"}, + expectedResponse: 0, + expectedValue: map[string]string{}, + expectedError: errors.New("each field must have a corresponding value"), + }, + { + name: "8. Command too short", + key: "HsetKey8", + presetValue: nil, + command: []string{"HSET", "field1"}, + expectedResponse: 0, + expectedValue: map[string]string{}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case map[string]string: - command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} - for key, value := range test.presetValue.(map[string]string) { - command = append(command, []resp.Value{ - resp.StringValue(key), - resp.StringValue(value)}..., - ) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -177,335 +202,198 @@ func Test_HandleHSET(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - - // Check that all the values are what is expected - if err := client.WriteArray([]resp.Value{ - resp.StringValue("HGETALL"), - resp.StringValue(test.key), - }); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - for idx, field := range res.Array() { - if idx%2 == 0 { - if res.Array()[idx+1].String() != test.expectedValue[field.String()] { - t.Errorf( - "expected value \"%+v\" for field \"%s\", got \"%+v\"", - test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), - ) - } - } - } - }) - } -} - -func Test_HandleHINCRBY(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - // Tests for both HIncrBy and HIncrByFloat - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse string // Change count - expectedValue map[string]string - expectedError error - }{ - { - name: "1. Increment by integer on non-existent hash should create a new one", - key: "HincrbyKey1", - presetValue: nil, - command: []string{"HINCRBY", "HincrbyKey1", "field1", "1"}, - expectedResponse: "1", - expectedValue: map[string]string{"field1": "1"}, - expectedError: nil, - }, - { - name: "2. Increment by float on non-existent hash should create one", - key: "HincrbyKey2", - presetValue: nil, - command: []string{"HINCRBYFLOAT", "HincrbyKey2", "field1", "3.142"}, - expectedResponse: "3.142", - expectedValue: map[string]string{"field1": "3.142"}, - expectedError: nil, - }, - { - name: "3. Increment by integer on existing hash", - key: "HincrbyKey3", - presetValue: map[string]string{"field1": "1"}, - command: []string{"HINCRBY", "HincrbyKey3", "field1", "10"}, - expectedResponse: "11", - expectedValue: map[string]string{"field1": "11"}, - expectedError: nil, - }, - { - name: "4. Increment by float on an existing hash", - key: "HincrbyKey4", - presetValue: map[string]string{"field1": "3.142"}, - command: []string{"HINCRBYFLOAT", "HincrbyKey4", "field1", "3.142"}, - expectedResponse: "6.284", - expectedValue: map[string]string{"field1": "6.284"}, - expectedError: nil, - }, - { - name: "5. Command too short", - key: "HincrbyKey5", - presetValue: nil, - command: []string{"HINCRBY", "HincrbyKey5"}, - expectedResponse: "0", - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - key: "HincrbyKey6", - presetValue: nil, - command: []string{"HINCRBY", "HincrbyKey6", "field1", "23", "45"}, - expectedResponse: "0", - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "7. Error when increment by float does not pass valid float", - key: "HincrbyKey7", - presetValue: nil, - command: []string{"HINCRBYFLOAT", "HincrbyKey7", "field1", "three point one four two"}, - expectedResponse: "0", - expectedValue: nil, - expectedError: errors.New("increment must be a float"), - }, - { - name: "8. Error when increment does not pass valid integer", - key: "HincrbyKey8", - presetValue: nil, - command: []string{"HINCRBY", "HincrbyKey8", "field1", "three"}, - expectedResponse: "0", - expectedValue: nil, - expectedError: errors.New("increment must be an integer"), - }, - { - name: "9. Error when trying to increment on a key that is not a hash", - key: "HincrbyKey9", - presetValue: "Default value", - command: []string{"HINCRBY", "HincrbyKey9", "field1", "3"}, - expectedResponse: "0", - expectedValue: nil, - expectedError: errors.New("value at HincrbyKey9 is not a hash"), - }, - { - name: "10. Error when trying to increment a hash field that is not a number", - key: "HincrbyKey10", - presetValue: map[string]string{"field1": "value1"}, - command: []string{"HINCRBY", "HincrbyKey10", "field1", "3"}, - expectedResponse: "0", - expectedValue: nil, - expectedError: errors.New("value at field field1 is not a number"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } - expected = "ok" - case map[string]string: - command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} - for key, value := range test.presetValue.(map[string]string) { - command = append(command, []resp.Value{ - resp.StringValue(key), - resp.StringValue(value)}..., - ) - } - expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + return } - if err = client.WriteArray(command); err != nil { + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + + // Check that all the values are what is expected + if err := client.WriteArray([]resp.Value{ + resp.StringValue("HGETALL"), + resp.StringValue(test.key), + }); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if res.String() != test.expectedResponse { - t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) - } - - // Check that all the values are what is expected - if err := client.WriteArray([]resp.Value{ - resp.StringValue("HGETALL"), - resp.StringValue(test.key), - }); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - for idx, field := range res.Array() { - if idx%2 == 0 { - if res.Array()[idx+1].String() != test.expectedValue[field.String()] { - t.Errorf( - "expected value \"%+v\" for field \"%s\", got \"%+v\"", - test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), - ) - } - } - } - }) - } -} - -func Test_HandleHGET(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse []string // Change count - expectedValue map[string]string - expectedError error - }{ - { - name: "1. Get values from existing hash.", - key: "HgetKey1", - presetValue: map[string]string{"field1": "value1", "field2": "365", "field3": "3.142"}, - command: []string{"HGET", "HgetKey1", "field1", "field2", "field3", "field4"}, - expectedResponse: []string{"value1", "365", "3.142", ""}, - expectedValue: map[string]string{"field1": "value1", "field2": "365", "field3": "3.142"}, - expectedError: nil, - }, - { - name: "2. Return nil when attempting to get from non-existed key", - key: "HgetKey2", - presetValue: nil, - command: []string{"HGET", "HgetKey2", "field1"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: nil, - }, - { - name: "3. Error when trying to get from a value that is not a hash map", - key: "HgetKey3", - presetValue: "Default Value", - command: []string{"HGET", "HgetKey3", "field1"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New("value at HgetKey3 is not a hash"), - }, - { - name: "4. Command too short", - key: "HgetKey4", - presetValue: nil, - command: []string{"HGET", "HgetKey4"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + for idx, field := range res.Array() { + if idx%2 == 0 { + if res.Array()[idx+1].String() != test.expectedValue[field.String()] { + t.Errorf( + "expected value \"%+v\" for field \"%s\", got \"%+v\"", + test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), + ) + } + } + } + }) + } + }) + + t.Run("Test_HandleHINCRBY", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + // Tests for both HIncrBy and HIncrByFloat + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse string // Change count + expectedValue map[string]string + expectedError error + }{ + { + name: "1. Increment by integer on non-existent hash should create a new one", + key: "HincrbyKey1", + presetValue: nil, + command: []string{"HINCRBY", "HincrbyKey1", "field1", "1"}, + expectedResponse: "1", + expectedValue: map[string]string{"field1": "1"}, + expectedError: nil, + }, + { + name: "2. Increment by float on non-existent hash should create one", + key: "HincrbyKey2", + presetValue: nil, + command: []string{"HINCRBYFLOAT", "HincrbyKey2", "field1", "3.142"}, + expectedResponse: "3.142", + expectedValue: map[string]string{"field1": "3.142"}, + expectedError: nil, + }, + { + name: "3. Increment by integer on existing hash", + key: "HincrbyKey3", + presetValue: map[string]string{"field1": "1"}, + command: []string{"HINCRBY", "HincrbyKey3", "field1", "10"}, + expectedResponse: "11", + expectedValue: map[string]string{"field1": "11"}, + expectedError: nil, + }, + { + name: "4. Increment by float on an existing hash", + key: "HincrbyKey4", + presetValue: map[string]string{"field1": "3.142"}, + command: []string{"HINCRBYFLOAT", "HincrbyKey4", "field1", "3.142"}, + expectedResponse: "6.284", + expectedValue: map[string]string{"field1": "6.284"}, + expectedError: nil, + }, + { + name: "5. Command too short", + key: "HincrbyKey5", + presetValue: nil, + command: []string{"HINCRBY", "HincrbyKey5"}, + expectedResponse: "0", + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + key: "HincrbyKey6", + presetValue: nil, + command: []string{"HINCRBY", "HincrbyKey6", "field1", "23", "45"}, + expectedResponse: "0", + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "7. Error when increment by float does not pass valid float", + key: "HincrbyKey7", + presetValue: nil, + command: []string{"HINCRBYFLOAT", "HincrbyKey7", "field1", "three point one four two"}, + expectedResponse: "0", + expectedValue: nil, + expectedError: errors.New("increment must be a float"), + }, + { + name: "8. Error when increment does not pass valid integer", + key: "HincrbyKey8", + presetValue: nil, + command: []string{"HINCRBY", "HincrbyKey8", "field1", "three"}, + expectedResponse: "0", + expectedValue: nil, + expectedError: errors.New("increment must be an integer"), + }, + { + name: "9. Error when trying to increment on a key that is not a hash", + key: "HincrbyKey9", + presetValue: "Default value", + command: []string{"HINCRBY", "HincrbyKey9", "field1", "3"}, + expectedResponse: "0", + expectedValue: nil, + expectedError: errors.New("value at HincrbyKey9 is not a hash"), + }, + { + name: "10. Error when trying to increment a hash field that is not a number", + key: "HincrbyKey10", + presetValue: map[string]string{"field1": "value1"}, + command: []string{"HINCRBY", "HincrbyKey10", "field1", "3"}, + expectedResponse: "0", + expectedValue: nil, + expectedError: errors.New("value at field field1 is not a number"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = "ok" - case map[string]string: - command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} - for key, value := range test.presetValue.(map[string]string) { - command = append(command, []resp.Value{ - resp.StringValue(key), - resp.StringValue(value)}..., - ) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -516,149 +404,143 @@ func Test_HandleHGET(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if res.String() != test.expectedResponse { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) } - return - } - if test.expectedResponse == nil { - if !res.IsNull() { - t.Errorf("expected nil response, got %+v", res) + // Check that all the values are what is expected + if err := client.WriteArray([]resp.Value{ + resp.StringValue("HGETALL"), + resp.StringValue(test.key), + }); err != nil { + t.Error(err) } - return - } - - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.String()) { - t.Errorf("unexpected element \"%s\" in response", item.String()) + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - } - - // Check that all the values are what is expected - if err := client.WriteArray([]resp.Value{ - resp.StringValue("HGETALL"), - resp.StringValue(test.key), - }); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - for idx, field := range res.Array() { - if idx%2 == 0 { - if res.Array()[idx+1].String() != test.expectedValue[field.String()] { - t.Errorf( - "expected value \"%+v\" for field \"%s\", got \"%+v\"", - test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), - ) + for idx, field := range res.Array() { + if idx%2 == 0 { + if res.Array()[idx+1].String() != test.expectedValue[field.String()] { + t.Errorf( + "expected value \"%+v\" for field \"%s\", got \"%+v\"", + test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), + ) + } } } - } - }) - } -} - -func Test_HandleHSTRLEN(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse []int // Change count - expectedValue map[string]string - expectedError error - }{ - { - // Return lengths of field values. - // If the key does not exist, its length should be 0. - name: "1. Return lengths of field values.", - key: "HstrlenKey1", - presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, - command: []string{"HSTRLEN", "HstrlenKey1", "field1", "field2", "field3", "field4"}, - expectedResponse: []int{len("value1"), len("123456789"), len("3.142"), 0}, - expectedValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, - expectedError: nil, - }, - { - name: "2. Nil response when trying to get HSTRLEN non-existent key", - key: "HstrlenKey2", - presetValue: nil, - command: []string{"HSTRLEN", "HstrlenKey2", "field1"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: nil, - }, - { - name: "3. Command too short", - key: "HstrlenKey3", - presetValue: nil, - command: []string{"HSTRLEN", "HstrlenKey3"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "4. Trying to get lengths on a non hash map returns error", - key: "HstrlenKey4", - presetValue: "Default value", - command: []string{"HSTRLEN", "HstrlenKey4", "field1"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New("value at HstrlenKey4 is not a hash"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + }) + } + }) + + t.Run("Test_HandleHGET", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse []string // Change count + expectedValue map[string]string + expectedError error + }{ + { + name: "1. Get values from existing hash.", + key: "HgetKey1", + presetValue: map[string]string{"field1": "value1", "field2": "365", "field3": "3.142"}, + command: []string{"HGET", "HgetKey1", "field1", "field2", "field3", "field4"}, + expectedResponse: []string{"value1", "365", "3.142", ""}, + expectedValue: map[string]string{"field1": "value1", "field2": "365", "field3": "3.142"}, + expectedError: nil, + }, + { + name: "2. Return nil when attempting to get from non-existed key", + key: "HgetKey2", + presetValue: nil, + command: []string{"HGET", "HgetKey2", "field1"}, + expectedResponse: nil, + expectedValue: nil, + expectedError: nil, + }, + { + name: "3. Error when trying to get from a value that is not a hash map", + key: "HgetKey3", + presetValue: "Default Value", + command: []string{"HGET", "HgetKey3", "field1"}, + expectedResponse: nil, + expectedValue: nil, + expectedError: errors.New("value at HgetKey3 is not a hash"), + }, + { + name: "4. Command too short", + key: "HgetKey4", + presetValue: nil, + command: []string{"HGET", "HgetKey4"}, + expectedResponse: nil, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case map[string]string: - command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} - for key, value := range test.presetValue.(map[string]string) { - command = append(command, []resp.Value{ - resp.StringValue(key), - resp.StringValue(value)}..., - ) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -669,156 +551,154 @@ func Test_HandleHSTRLEN(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if test.expectedResponse == nil { + if !res.IsNull() { + t.Errorf("expected nil response, got %+v", res) + } + return } - return - } - if test.expectedResponse == nil { - if !res.IsNull() { - t.Errorf("expected nil response, got %+v", res) + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } } - return - } - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.Integer()) { - t.Errorf("unexpected element \"%d\" in response", item.Integer()) + // Check that all the values are what is expected + if err := client.WriteArray([]resp.Value{ + resp.StringValue("HGETALL"), + resp.StringValue(test.key), + }); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - } - - // Check that all the values are what is expected - if err := client.WriteArray([]resp.Value{ - resp.StringValue("HGETALL"), - resp.StringValue(test.key), - }); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - for idx, field := range res.Array() { - if idx%2 == 0 { - if res.Array()[idx+1].String() != test.expectedValue[field.String()] { - t.Errorf( - "expected value \"%+v\" for field \"%s\", got \"%+v\"", - test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), - ) + for idx, field := range res.Array() { + if idx%2 == 0 { + if res.Array()[idx+1].String() != test.expectedValue[field.String()] { + t.Errorf( + "expected value \"%+v\" for field \"%s\", got \"%+v\"", + test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), + ) + } } } - } - }) - } -} - -func Test_HandleHVALS(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse []string - expectedValue map[string]string - expectedError error - }{ - { - name: "1. Return all the values from a hash", - key: "HvalsKey1", - presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, - command: []string{"HVALS", "HvalsKey1"}, - expectedResponse: []string{"value1", "123456789", "3.142"}, - expectedValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, - expectedError: nil, - }, - { - name: "2. Empty array response when trying to get HSTRLEN non-existent key", - key: "HvalsKey2", - presetValue: nil, - command: []string{"HVALS", "HvalsKey2"}, - expectedResponse: []string{}, - expectedValue: nil, - expectedError: nil, - }, - { - name: "3. Command too short", - key: "HvalsKey3", - presetValue: nil, - command: []string{"HVALS"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "4. Command too long", - key: "HvalsKey4", - presetValue: nil, - command: []string{"HVALS", "HvalsKey4", "HvalsKey4"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Trying to get lengths on a non hash map returns error", - key: "HvalsKey5", - presetValue: "Default value", - command: []string{"HVALS", "HvalsKey5"}, - expectedResponse: nil, - expectedValue: nil, - expectedError: errors.New("value at HvalsKey5 is not a hash"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + }) + } + }) + + t.Run("Test_HandleHSTRLEN", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse []int // Change count + expectedValue map[string]string + expectedError error + }{ + { + // Return lengths of field values. + // If the key does not exist, its length should be 0. + name: "1. Return lengths of field values.", + key: "HstrlenKey1", + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, + command: []string{"HSTRLEN", "HstrlenKey1", "field1", "field2", "field3", "field4"}, + expectedResponse: []int{len("value1"), len("123456789"), len("3.142"), 0}, + expectedValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, + expectedError: nil, + }, + { + name: "2. Nil response when trying to get HSTRLEN non-existent key", + key: "HstrlenKey2", + presetValue: nil, + command: []string{"HSTRLEN", "HstrlenKey2", "field1"}, + expectedResponse: nil, + expectedValue: nil, + expectedError: nil, + }, + { + name: "3. Command too short", + key: "HstrlenKey3", + presetValue: nil, + command: []string{"HSTRLEN", "HstrlenKey3"}, + expectedResponse: nil, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "4. Trying to get lengths on a non hash map returns error", + key: "HstrlenKey4", + presetValue: "Default value", + command: []string{"HSTRLEN", "HstrlenKey4", "field1"}, + expectedResponse: nil, + expectedValue: nil, + expectedError: errors.New("value at HstrlenKey4 is not a hash"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case map[string]string: - command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} - for key, value := range test.presetValue.(map[string]string) { - command = append(command, []resp.Value{ - resp.StringValue(key), - resp.StringValue(value)}..., - ) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -829,200 +709,161 @@ func Test_HandleHVALS(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if test.expectedResponse == nil { + if !res.IsNull() { + t.Errorf("expected nil response, got %+v", res) + } + return } - return - } - if test.expectedResponse == nil { - if !res.IsNull() { - t.Errorf("expected nil response, got %+v", res) + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.Integer()) { + t.Errorf("unexpected element \"%d\" in response", item.Integer()) + } } - return - } - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.String()) { - t.Errorf("unexpected element \"%s\" in response", item.String()) + // Check that all the values are what is expected + if err := client.WriteArray([]resp.Value{ + resp.StringValue("HGETALL"), + resp.StringValue(test.key), + }); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - } - }) - } -} - -func Test_HandleHRANDFIELD(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse []string - expectedError error - }{ - { - name: "1. Get a random field", - key: "HrandfieldKey1", - presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, - command: []string{"HRANDFIELD", "HrandfieldKey1"}, - expectedResponse: []string{"field1", "field2", "field3"}, - expectedError: nil, - }, - { - name: "2. Get a random field with a value", - key: "HrandfieldKey2", - presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, - command: []string{"HRANDFIELD", "HrandfieldKey2", "1", "WITHVALUES"}, - expectedResponse: []string{"field1", "value1", "field2", "123456789", "field3", "3.142"}, - expectedError: nil, - }, - { - name: "3. Get several random fields", - key: "HrandfieldKey3", - presetValue: map[string]string{ - "field1": "value1", - "field2": "123456789", - "field3": "3.142", - "field4": "value4", - "field5": "value5", - }, - command: []string{"HRANDFIELD", "HrandfieldKey3", "3"}, - expectedResponse: []string{"field1", "field2", "field3", "field4", "field5"}, - expectedError: nil, - }, - { - name: "4. Get several random fields with their corresponding values", - key: "HrandfieldKey4", - presetValue: map[string]string{ - "field1": "value1", - "field2": "123456789", - "field3": "3.142", - "field4": "value4", - "field5": "value5", - }, - command: []string{"HRANDFIELD", "HrandfieldKey4", "3", "WITHVALUES"}, - expectedResponse: []string{ - "field1", "value1", "field2", "123456789", "field3", - "3.142", "field4", "value4", "field5", "value5", - }, - expectedError: nil, - }, - { - name: "5. Get the entire hash", - key: "HrandfieldKey5", - presetValue: map[string]string{ - "field1": "value1", - "field2": "123456789", - "field3": "3.142", - "field4": "value4", - "field5": "value5", - }, - command: []string{"HRANDFIELD", "HrandfieldKey5", "5"}, - expectedResponse: []string{"field1", "field2", "field3", "field4", "field5"}, - expectedError: nil, - }, - { - name: "6. Get the entire hash with values", - key: "HrandfieldKey5", - presetValue: map[string]string{ - "field1": "value1", - "field2": "123456789", - "field3": "3.142", - "field4": "value4", - "field5": "value5", - }, - command: []string{"HRANDFIELD", "HrandfieldKey5", "5", "WITHVALUES"}, - expectedResponse: []string{ - "field1", "value1", "field2", "123456789", "field3", - "3.142", "field4", "value4", "field5", "value5", - }, - expectedError: nil, - }, - { - name: "7. Command too short", - key: "HrandfieldKey10", - presetValue: nil, - command: []string{"HRANDFIELD"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "8. Command too long", - key: "HrandfieldKey11", - presetValue: nil, - command: []string{"HRANDFIELD", "HrandfieldKey11", "HrandfieldKey11", "HrandfieldKey11", "HrandfieldKey11"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "9. Trying to get random field on a non hash map returns error", - key: "HrandfieldKey12", - presetValue: "Default value", - command: []string{"HRANDFIELD", "HrandfieldKey12"}, - expectedError: errors.New("value at HrandfieldKey12 is not a hash"), - }, - { - name: "10. Throw error when count provided is not an integer", - key: "HrandfieldKey12", - presetValue: "Default value", - command: []string{"HRANDFIELD", "HrandfieldKey12", "COUNT"}, - expectedError: errors.New("count must be an integer"), - }, - { - name: "11. If fourth argument is provided, it must be \"WITHVALUES\"", - key: "HrandfieldKey12", - presetValue: "Default value", - command: []string{"HRANDFIELD", "HrandfieldKey12", "10", "FLAG"}, - expectedError: errors.New("result modifier must be withvalues"), - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + for idx, field := range res.Array() { + if idx%2 == 0 { + if res.Array()[idx+1].String() != test.expectedValue[field.String()] { + t.Errorf( + "expected value \"%+v\" for field \"%s\", got \"%+v\"", + test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), + ) + } + } + } + }) + } + }) + + t.Run("Test_HandleHVALS", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse []string + expectedValue map[string]string + expectedError error + }{ + { + name: "1. Return all the values from a hash", + key: "HvalsKey1", + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, + command: []string{"HVALS", "HvalsKey1"}, + expectedResponse: []string{"value1", "123456789", "3.142"}, + expectedValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, + expectedError: nil, + }, + { + name: "2. Empty array response when trying to get HSTRLEN non-existent key", + key: "HvalsKey2", + presetValue: nil, + command: []string{"HVALS", "HvalsKey2"}, + expectedResponse: []string{}, + expectedValue: nil, + expectedError: nil, + }, + { + name: "3. Command too short", + key: "HvalsKey3", + presetValue: nil, + command: []string{"HVALS"}, + expectedResponse: nil, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "4. Command too long", + key: "HvalsKey4", + presetValue: nil, + command: []string{"HVALS", "HvalsKey4", "HvalsKey4"}, + expectedResponse: nil, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Trying to get lengths on a non hash map returns error", + key: "HvalsKey5", + presetValue: "Default value", + command: []string{"HVALS", "HvalsKey5"}, + expectedResponse: nil, + expectedValue: nil, + expectedError: errors.New("value at HvalsKey5 is not a hash"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case map[string]string: - command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} - for key, value := range test.presetValue.(map[string]string) { - command = append(command, []resp.Value{ - resp.StringValue(key), - resp.StringValue(value)}..., - ) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -1033,126 +874,205 @@ func Test_HandleHRANDFIELD(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - return - } - if test.expectedResponse == nil { - if !res.IsNull() { - t.Errorf("expected nil response, got %+v", res) + if test.expectedResponse == nil { + if !res.IsNull() { + t.Errorf("expected nil response, got %+v", res) + } + return } - return - } - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.String()) { - t.Errorf("unexpected element \"%s\" in response", item.String()) + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } } - } - }) - } -} - -func Test_HandleHLEN(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse int // Change count - expectedError error - }{ - { - name: "1. Return the correct length of the hash", - key: "HlenKey1", - presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, - command: []string{"HLEN", "HlenKey1"}, - expectedResponse: 3, - expectedError: nil, - }, - { - name: "2. 0 response when trying to call HLEN on non-existent key", - key: "HlenKey2", - presetValue: nil, - command: []string{"HLEN", "HlenKey2"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "3. Command too short", - key: "HlenKey3", - presetValue: nil, - command: []string{"HLEN"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "4. Command too long", - presetValue: nil, - command: []string{"HLEN", "HlenKey4", "HlenKey4"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Trying to get lengths on a non hash map returns error", - key: "HlenKey5", - presetValue: "Default value", - command: []string{"HLEN", "HlenKey5"}, - expectedResponse: 0, - expectedError: errors.New("value at HlenKey5 is not a hash"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + }) + } + }) + + t.Run("Test_HandleHRANDFIELD", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse []string + expectedError error + }{ + { + name: "1. Get a random field", + key: "HrandfieldKey1", + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, + command: []string{"HRANDFIELD", "HrandfieldKey1"}, + expectedResponse: []string{"field1", "field2", "field3"}, + expectedError: nil, + }, + { + name: "2. Get a random field with a value", + key: "HrandfieldKey2", + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, + command: []string{"HRANDFIELD", "HrandfieldKey2", "1", "WITHVALUES"}, + expectedResponse: []string{"field1", "value1", "field2", "123456789", "field3", "3.142"}, + expectedError: nil, + }, + { + name: "3. Get several random fields", + key: "HrandfieldKey3", + presetValue: map[string]string{ + "field1": "value1", + "field2": "123456789", + "field3": "3.142", + "field4": "value4", + "field5": "value5", + }, + command: []string{"HRANDFIELD", "HrandfieldKey3", "3"}, + expectedResponse: []string{"field1", "field2", "field3", "field4", "field5"}, + expectedError: nil, + }, + { + name: "4. Get several random fields with their corresponding values", + key: "HrandfieldKey4", + presetValue: map[string]string{ + "field1": "value1", + "field2": "123456789", + "field3": "3.142", + "field4": "value4", + "field5": "value5", + }, + command: []string{"HRANDFIELD", "HrandfieldKey4", "3", "WITHVALUES"}, + expectedResponse: []string{ + "field1", "value1", "field2", "123456789", "field3", + "3.142", "field4", "value4", "field5", "value5", + }, + expectedError: nil, + }, + { + name: "5. Get the entire hash", + key: "HrandfieldKey5", + presetValue: map[string]string{ + "field1": "value1", + "field2": "123456789", + "field3": "3.142", + "field4": "value4", + "field5": "value5", + }, + command: []string{"HRANDFIELD", "HrandfieldKey5", "5"}, + expectedResponse: []string{"field1", "field2", "field3", "field4", "field5"}, + expectedError: nil, + }, + { + name: "6. Get the entire hash with values", + key: "HrandfieldKey5", + presetValue: map[string]string{ + "field1": "value1", + "field2": "123456789", + "field3": "3.142", + "field4": "value4", + "field5": "value5", + }, + command: []string{"HRANDFIELD", "HrandfieldKey5", "5", "WITHVALUES"}, + expectedResponse: []string{ + "field1", "value1", "field2", "123456789", "field3", + "3.142", "field4", "value4", "field5", "value5", + }, + expectedError: nil, + }, + { + name: "7. Command too short", + key: "HrandfieldKey10", + presetValue: nil, + command: []string{"HRANDFIELD"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "8. Command too long", + key: "HrandfieldKey11", + presetValue: nil, + command: []string{"HRANDFIELD", "HrandfieldKey11", "HrandfieldKey11", "HrandfieldKey11", "HrandfieldKey11"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "9. Trying to get random field on a non hash map returns error", + key: "HrandfieldKey12", + presetValue: "Default value", + command: []string{"HRANDFIELD", "HrandfieldKey12"}, + expectedError: errors.New("value at HrandfieldKey12 is not a hash"), + }, + { + name: "10. Throw error when count provided is not an integer", + key: "HrandfieldKey12", + presetValue: "Default value", + command: []string{"HRANDFIELD", "HrandfieldKey12", "COUNT"}, + expectedError: errors.New("count must be an integer"), + }, + { + name: "11. If fourth argument is provided, it must be \"WITHVALUES\"", + key: "HrandfieldKey12", + presetValue: "Default value", + command: []string{"HRANDFIELD", "HrandfieldKey12", "10", "FLAG"}, + expectedError: errors.New("result modifier must be withvalues"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = "ok" - case map[string]string: - command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} - for key, value := range test.presetValue.(map[string]string) { - command = append(command, []resp.Value{ - resp.StringValue(key), - resp.StringValue(value)}..., - ) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -1163,117 +1083,131 @@ func Test_HandleHLEN(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if test.expectedResponse == nil { + if !res.IsNull() { + t.Errorf("expected nil response, got %+v", res) + } + return } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - }) - } -} - -func Test_HandleHKeys(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse []string - expectedError error - }{ - { - name: "1. Return an array containing all the keys of the hash", - key: "HkeysKey1", - presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, - command: []string{"HKEYS", "HkeysKey1"}, - expectedResponse: []string{"field1", "field2", "field3"}, - expectedError: nil, - }, - { - name: "2. Empty array response when trying to call HKEYS on non-existent key", - key: "HkeysKey2", - presetValue: nil, - command: []string{"HKEYS", "HkeysKey2"}, - expectedResponse: []string{}, - expectedError: nil, - }, - { - name: "3. Command too short", - key: "HkeysKey3", - presetValue: nil, - command: []string{"HKEYS"}, - expectedResponse: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "4. Command too long", - key: "HkeysKey4", - presetValue: nil, - command: []string{"HKEYS", "HkeysKey4", "HkeysKey4"}, - expectedResponse: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Trying to get lengths on a non hash map returns error", - key: "HkeysKey5", - presetValue: "Default value", - command: []string{"HKEYS", "HkeysKey5"}, - expectedError: errors.New("value at HkeysKey5 is not a hash"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } + } + }) + } + }) + + t.Run("Test_HandleHLEN", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse int // Change count + expectedError error + }{ + { + name: "1. Return the correct length of the hash", + key: "HlenKey1", + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, + command: []string{"HLEN", "HlenKey1"}, + expectedResponse: 3, + expectedError: nil, + }, + { + name: "2. 0 response when trying to call HLEN on non-existent key", + key: "HlenKey2", + presetValue: nil, + command: []string{"HLEN", "HlenKey2"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "3. Command too short", + key: "HlenKey3", + presetValue: nil, + command: []string{"HLEN"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "4. Command too long", + presetValue: nil, + command: []string{"HLEN", "HlenKey4", "HlenKey4"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Trying to get lengths on a non hash map returns error", + key: "HlenKey5", + presetValue: "Default value", + command: []string{"HLEN", "HlenKey5"}, + expectedResponse: 0, + expectedError: errors.New("value at HlenKey5 is not a hash"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case map[string]string: - command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} - for key, value := range test.presetValue.(map[string]string) { - command = append(command, []resp.Value{ - resp.StringValue(key), - resp.StringValue(value)}..., - ) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -1284,120 +1218,251 @@ func Test_HandleHKeys(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleHKeys", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse []string + expectedError error + }{ + { + name: "1. Return an array containing all the keys of the hash", + key: "HkeysKey1", + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, + command: []string{"HKEYS", "HkeysKey1"}, + expectedResponse: []string{"field1", "field2", "field3"}, + expectedError: nil, + }, + { + name: "2. Empty array response when trying to call HKEYS on non-existent key", + key: "HkeysKey2", + presetValue: nil, + command: []string{"HKEYS", "HkeysKey2"}, + expectedResponse: []string{}, + expectedError: nil, + }, + { + name: "3. Command too short", + key: "HkeysKey3", + presetValue: nil, + command: []string{"HKEYS"}, + expectedResponse: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "4. Command too long", + key: "HkeysKey4", + presetValue: nil, + command: []string{"HKEYS", "HkeysKey4", "HkeysKey4"}, + expectedResponse: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Trying to get lengths on a non hash map returns error", + key: "HkeysKey5", + presetValue: "Default value", + command: []string{"HKEYS", "HkeysKey5"}, + expectedError: errors.New("value at HkeysKey5 is not a hash"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - return - } - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.String()) { - t.Errorf("unexpected value \"%s\" in response", item.String()) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - } - }) - } -} -func Test_HandleHGETALL(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse map[string]string - expectedError error - }{ - { - name: "1. Return an array containing all the fields and values of the hash", - key: "HGetAllKey1", - presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, - command: []string{"HGETALL", "HGetAllKey1"}, - expectedResponse: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, - expectedError: nil, - }, - { - name: "2. Empty array response when trying to call HGETALL on non-existent key", - key: "HGetAllKey2", - presetValue: nil, - command: []string{"HGETALL", "HGetAllKey2"}, - expectedResponse: nil, - expectedError: nil, - }, - { - name: "3. Command too short", - key: "HGetAllKey3", - presetValue: nil, - command: []string{"HGETALL"}, - expectedResponse: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "4. Command too long", - key: "HGetAllKey4", - presetValue: nil, - command: []string{"HGETALL", "HGetAllKey4", "HGetAllKey4"}, - expectedResponse: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Trying to get lengths on a non hash map returns error", - key: "HGetAllKey5", - presetValue: "Default value", - command: []string{"HGETALL", "HGetAllKey5"}, - expectedResponse: nil, - expectedError: errors.New("value at HGetAllKey5 is not a hash"), - }, - } + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected value \"%s\" in response", item.String()) + } + } + }) + } + }) + + t.Run("Test_HandleHGETALL", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse map[string]string + expectedError error + }{ + { + name: "1. Return an array containing all the fields and values of the hash", + key: "HGetAllKey1", + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, + command: []string{"HGETALL", "HGetAllKey1"}, + expectedResponse: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, + expectedError: nil, + }, + { + name: "2. Empty array response when trying to call HGETALL on non-existent key", + key: "HGetAllKey2", + presetValue: nil, + command: []string{"HGETALL", "HGetAllKey2"}, + expectedResponse: nil, + expectedError: nil, + }, + { + name: "3. Command too short", + key: "HGetAllKey3", + presetValue: nil, + command: []string{"HGETALL"}, + expectedResponse: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "4. Command too long", + key: "HGetAllKey4", + presetValue: nil, + command: []string{"HGETALL", "HGetAllKey4", "HGetAllKey4"}, + expectedResponse: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Trying to get lengths on a non hash map returns error", + key: "HGetAllKey5", + presetValue: "Default value", + command: []string{"HGETALL", "HGetAllKey5"}, + expectedResponse: nil, + expectedError: errors.New("value at HGetAllKey5 is not a hash"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case map[string]string: - command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} - for key, value := range test.presetValue.(map[string]string) { - command = append(command, []resp.Value{ - resp.StringValue(key), - resp.StringValue(value)}..., - ) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -1408,132 +1473,137 @@ func Test_HandleHGETALL(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - return - } - if test.expectedResponse == nil { - if len(res.Array()) != 0 { - t.Errorf("expected response to be empty array, got %+v", res) + if test.expectedResponse == nil { + if len(res.Array()) != 0 { + t.Errorf("expected response to be empty array, got %+v", res) + } + return } - return - } - for i, item := range res.Array() { - if i%2 == 0 { - field := item.String() - value := res.Array()[i+1].String() - if test.expectedResponse[field] != value { - t.Errorf("expected value at field \"%s\" to be \"%s\", got \"%s\"", field, test.expectedResponse[field], value) + for i, item := range res.Array() { + if i%2 == 0 { + field := item.String() + value := res.Array()[i+1].String() + if test.expectedResponse[field] != value { + t.Errorf("expected value at field \"%s\" to be \"%s\", got \"%s\"", field, test.expectedResponse[field], value) + } } } - } - - }) - } -} -func Test_HandleHEXISTS(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse bool - expectedError error - }{ - { - name: "1. Return 1 if the field exists in the hash", - key: "HexistsKey1", - presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, - command: []string{"HEXISTS", "HexistsKey1", "field1"}, - expectedResponse: true, - expectedError: nil, - }, - { - name: "2. 0 response when trying to call HEXISTS on non-existent key", - key: "HexistsKey2", - presetValue: nil, - command: []string{"HEXISTS", "HexistsKey2", "field1"}, - expectedResponse: false, - expectedError: nil, - }, - { - name: "3. Command too short", - key: "HexistsKey3", - presetValue: nil, - command: []string{"HEXISTS", "HexistsKey3"}, - expectedResponse: false, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "4. Command too long", - key: "HexistsKey4", - presetValue: nil, - command: []string{"HEXISTS", "HexistsKey4", "field1", "field2"}, - expectedResponse: false, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Trying to get lengths on a non hash map returns error", - key: "HexistsKey5", - presetValue: "Default value", - command: []string{"HEXISTS", "HexistsKey5", "field1"}, - expectedResponse: false, - expectedError: errors.New("value at HexistsKey5 is not a hash"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + }) + } + }) + + t.Run("Test_HandleHEXISTS", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse bool + expectedError error + }{ + { + name: "1. Return 1 if the field exists in the hash", + key: "HexistsKey1", + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142"}, + command: []string{"HEXISTS", "HexistsKey1", "field1"}, + expectedResponse: true, + expectedError: nil, + }, + { + name: "2. 0 response when trying to call HEXISTS on non-existent key", + key: "HexistsKey2", + presetValue: nil, + command: []string{"HEXISTS", "HexistsKey2", "field1"}, + expectedResponse: false, + expectedError: nil, + }, + { + name: "3. Command too short", + key: "HexistsKey3", + presetValue: nil, + command: []string{"HEXISTS", "HexistsKey3"}, + expectedResponse: false, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "4. Command too long", + key: "HexistsKey4", + presetValue: nil, + command: []string{"HEXISTS", "HexistsKey4", "field1", "field2"}, + expectedResponse: false, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Trying to get lengths on a non hash map returns error", + key: "HexistsKey5", + presetValue: "Default value", + command: []string{"HEXISTS", "HexistsKey5", "field1"}, + expectedResponse: false, + expectedError: errors.New("value at HexistsKey5 is not a hash"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case map[string]string: - command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} - for key, value := range test.presetValue.(map[string]string) { - command = append(command, []resp.Value{ - resp.StringValue(key), - resp.StringValue(value)}..., - ) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -1544,124 +1614,129 @@ func Test_HandleHEXISTS(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if res.Bool() != test.expectedResponse { + t.Errorf("expected response to be %v, got %v", test.expectedResponse, res.Bool()) + } + }) + } + }) + + t.Run("Test_HandleHDEL", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse int + expectedValue map[string]string + expectedError error + }{ + { + name: "1. Return count of deleted fields in the specified hash", + key: "HdelKey1", + presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142", "field7": "value7"}, + command: []string{"HDEL", "HdelKey1", "field1", "field2", "field3", "field4", "field5", "field6"}, + expectedResponse: 3, + expectedValue: map[string]string{"field7": "value7"}, + expectedError: nil, + }, + { + name: "2. 0 response when passing delete fields that are non-existent on valid hash", + key: "HdelKey2", + presetValue: map[string]string{"field1": "value1", "field2": "value2", "field3": "value3"}, + command: []string{"HDEL", "HdelKey2", "field4", "field5", "field6"}, + expectedResponse: 0, + expectedValue: map[string]string{"field1": "value1", "field2": "value2", "field3": "value3"}, + expectedError: nil, + }, + { + name: "3. 0 response when trying to call HDEL on non-existent key", + key: "HdelKey3", + presetValue: nil, + command: []string{"HDEL", "HdelKey3", "field1"}, + expectedResponse: 0, + expectedValue: nil, + expectedError: nil, + }, + { + name: "4. Command too short", + key: "HdelKey4", + presetValue: nil, + command: []string{"HDEL", "HdelKey4"}, + expectedResponse: 0, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Trying to get lengths on a non hash map returns error", + key: "HdelKey5", + presetValue: "Default value", + command: []string{"HDEL", "HdelKey5", "field1"}, + expectedResponse: 0, + expectedValue: nil, + expectedError: errors.New("value at HdelKey5 is not a hash"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case map[string]string: + command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} + for key, value := range test.presetValue.(map[string]string) { + command = append(command, []resp.Value{ + resp.StringValue(key), + resp.StringValue(value)}..., + ) + } + expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - return - } - - if res.Bool() != test.expectedResponse { - t.Errorf("expected response to be %v, got %v", test.expectedResponse, res.Bool()) - } - }) - } -} - -func Test_HandleHDEL(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse int - expectedValue map[string]string - expectedError error - }{ - { - name: "1. Return count of deleted fields in the specified hash", - key: "HdelKey1", - presetValue: map[string]string{"field1": "value1", "field2": "123456789", "field3": "3.142", "field7": "value7"}, - command: []string{"HDEL", "HdelKey1", "field1", "field2", "field3", "field4", "field5", "field6"}, - expectedResponse: 3, - expectedValue: map[string]string{"field7": "value7"}, - expectedError: nil, - }, - { - name: "2. 0 response when passing delete fields that are non-existent on valid hash", - key: "HdelKey2", - presetValue: map[string]string{"field1": "value1", "field2": "value2", "field3": "value3"}, - command: []string{"HDEL", "HdelKey2", "field4", "field5", "field6"}, - expectedResponse: 0, - expectedValue: map[string]string{"field1": "value1", "field2": "value2", "field3": "value3"}, - expectedError: nil, - }, - { - name: "3. 0 response when trying to call HDEL on non-existent key", - key: "HdelKey3", - presetValue: nil, - command: []string{"HDEL", "HdelKey3", "field1"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: nil, - }, - { - name: "4. Command too short", - key: "HdelKey4", - presetValue: nil, - command: []string{"HDEL", "HdelKey4"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Trying to get lengths on a non hash map returns error", - key: "HdelKey5", - presetValue: "Default value", - command: []string{"HDEL", "HdelKey5", "field1"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("value at HdelKey5 is not a hash"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), - } - expected = "ok" - case map[string]string: - command = []resp.Value{resp.StringValue("HSET"), resp.StringValue(test.key)} - for key, value := range test.presetValue.(map[string]string) { - command = append(command, []resp.Value{ - resp.StringValue(key), - resp.StringValue(value)}..., - ) - } - expected = strconv.Itoa(len(test.presetValue.(map[string]string))) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -1672,45 +1747,28 @@ func Test_HandleHDEL(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - - for idx, field := range res.Array() { - if idx%2 == 0 { - if res.Array()[idx+1].String() != test.expectedValue[field.String()] { - t.Errorf( - "expected value \"%+v\" for field \"%s\", got \"%+v\"", - test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), - ) + for idx, field := range res.Array() { + if idx%2 == 0 { + if res.Array()[idx+1].String() != test.expectedValue[field.String()] { + t.Errorf( + "expected value \"%+v\" for field \"%s\", got \"%+v\"", + test.expectedValue[field.String()], field.String(), res.Array()[idx+1].String(), + ) + } } } - } - }) - } + }) + } + }) } diff --git a/internal/modules/list/commands_test.go b/internal/modules/list/commands_test.go index d8f7c402..8ff348ea 100644 --- a/internal/modules/list/commands_test.go +++ b/internal/modules/list/commands_test.go @@ -16,120 +16,145 @@ package list_test import ( "errors" - "fmt" "github.com/echovault/echovault/echovault" "github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/constants" "github.com/tidwall/resp" - "net" "slices" "strconv" "strings" - "sync" "testing" ) -var mockServer *echovault.EchoVault -var addr = "localhost" -var port int +func Test_List(t *testing.T) { + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } -func init() { - port, _ = internal.GetFreePort() - mockServer, _ = echovault.NewEchoVault( + mockServer, err := echovault.NewEchoVault( echovault.WithConfig(config.Config{ - BindAddr: addr, + BindAddr: "localhost", Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) - wg := sync.WaitGroup{} - wg.Add(1) + if err != nil { + t.Error(err) + return + } + go func() { - wg.Done() mockServer.Start() }() - wg.Wait() -} -func Test_HandleLLEN(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse int - expectedError error - }{ - { - name: "1. If key exists and is a list, return the lists length", - key: "LlenKey1", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"LLEN", "LlenKey1"}, - expectedResponse: 4, - expectedError: nil, - }, - { - name: "2. If key does not exist, return 0", - key: "LlenKey2", - presetValue: nil, - command: []string{"LLEN", "LlenKey2"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "3. Command too short", - key: "LlenKey3", - presetValue: nil, - command: []string{"LLEN"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "4. Command too long", - key: "LlenKey4", - presetValue: nil, - command: []string{"LLEN", "LlenKey4", "LlenKey4"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Trying to get lengths on a non-list returns error", - key: "LlenKey5", - presetValue: "Default value", - command: []string{"LLEN", "LlenKey5"}, - expectedResponse: 0, - expectedError: errors.New("LLEN command on non-list item"), - }, - } + t.Cleanup(func() { + mockServer.ShutDown() + }) + + t.Run("Test_HandleLLEN", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse int + expectedError error + }{ + { + name: "1. If key exists and is a list, return the lists length", + key: "LlenKey1", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LLEN", "LlenKey1"}, + expectedResponse: 4, + expectedError: nil, + }, + { + name: "2. If key does not exist, return 0", + key: "LlenKey2", + presetValue: nil, + command: []string{"LLEN", "LlenKey2"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "3. Command too short", + key: "LlenKey3", + presetValue: nil, + command: []string{"LLEN"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "4. Command too long", + key: "LlenKey4", + presetValue: nil, + command: []string{"LLEN", "LlenKey4", "LlenKey4"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Trying to get lengths on a non-list returns error", + key: "LlenKey5", + presetValue: "Default value", + command: []string{"LLEN", "LlenKey5"}, + expectedResponse: 0, + expectedError: errors.New("LLEN command on non-list item"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = "ok" - case []string: - command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} - for _, element := range test.presetValue.([]string) { - command = append(command, []resp.Value{resp.StringValue(element)}...) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -140,155 +165,343 @@ func Test_HandleLLEN(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response to be %d, got %d", test.expectedResponse, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleLINDEX", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse string + expectedError error + }{ + { + name: "1. Return last element within range", + key: "LindexKey1", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LINDEX", "LindexKey1", "3"}, + expectedResponse: "value4", + expectedError: nil, + }, + { + name: "2. Return first element within range", + key: "LindexKey2", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LINDEX", "LindexKey1", "0"}, + expectedResponse: "value1", + expectedError: nil, + }, + { + name: "3. Return middle element within range", + key: "LindexKey3", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LINDEX", "LindexKey1", "1"}, + expectedResponse: "value2", + expectedError: nil, + }, + { + name: "4. If key does not exist, return error", + key: "LindexKey4", + presetValue: nil, + command: []string{"LINDEX", "LindexKey4", "0"}, + expectedResponse: "", + expectedError: errors.New("LINDEX command on non-list item"), + }, + { + name: "5. Command too short", + key: "LindexKey3", + presetValue: nil, + command: []string{"LINDEX", "LindexKey3"}, + expectedResponse: "", + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: " 6. Command too long", + key: "LindexKey4", + presetValue: nil, + command: []string{"LINDEX", "LindexKey4", "0", "20"}, + expectedResponse: "", + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "7. Trying to get element by index on a non-list returns error", + key: "LindexKey5", + presetValue: "Default value", + command: []string{"LINDEX", "LindexKey5", "0"}, + expectedResponse: "", + expectedError: errors.New("LINDEX command on non-list item"), + }, + { + name: "8. Trying to get index out of range index beyond last index", + key: "LindexKey6", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LINDEX", "LindexKey6", "3"}, + expectedResponse: "", + expectedError: errors.New("index must be within list range"), + }, + { + name: "9. Trying to get index out of range with negative index", + key: "LindexKey7", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LINDEX", "LindexKey7", "-1"}, + expectedResponse: "", + expectedError: errors.New("index must be within list range"), + }, + { + name: " 10. Return error when index is not an integer", + key: "LindexKey8", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LINDEX", "LindexKey8", "index"}, + expectedResponse: "", + expectedError: errors.New("index must be an integer"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response to be %d, got %d", test.expectedResponse, res.Integer()) - } - }) - } -} + if res.String() != test.expectedResponse { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) + } + }) + } + }) + + t.Run("Test_HandleLRANGE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse []string + expectedError error + }{ + { + // Return sub-list within range. + // Both start and end indices are positive. + // End index is greater than start index. + name: "1. Return sub-list within range.", + key: "LrangeKey1", + presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, + command: []string{"LRANGE", "LrangeKey1", "3", "6"}, + expectedResponse: []string{"value4", "value5", "value6", "value7"}, + expectedError: nil, + }, + { + name: "2. Return sub-list from start index to the end of the list when end index is -1", + key: "LrangeKey2", + presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, + command: []string{"LRANGE", "LrangeKey2", "3", "-1"}, + expectedResponse: []string{"value4", "value5", "value6", "value7", "value8"}, + expectedError: nil, + }, + { + name: "3. Return the reversed sub-list when the end index is greater than -1 but less than start index", + key: "LrangeKey3", + presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, + command: []string{"LRANGE", "LrangeKey3", "3", "0"}, + expectedResponse: []string{"value4", "value3", "value2", "value1"}, + expectedError: nil, + }, + { + name: "4. If key does not exist, return error", + key: "LrangeKey4", + presetValue: nil, + command: []string{"LRANGE", "LrangeKey4", "0", "2"}, + expectedResponse: nil, + expectedError: errors.New("LRANGE command on non-list item"), + }, + { + name: "5. Command too short", + key: "LrangeKey5", + presetValue: nil, + command: []string{"LRANGE", "LrangeKey5"}, + expectedResponse: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + key: "LrangeKey6", + presetValue: nil, + command: []string{"LRANGE", "LrangeKey6", "0", "element", "element"}, + expectedResponse: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "7. Error when executing command on non-list command", + key: "LrangeKey5", + presetValue: "Default value", + command: []string{"LRANGE", "LrangeKey5", "0", "3"}, + expectedResponse: nil, + expectedError: errors.New("LRANGE command on non-list item"), + }, + { + name: "8. Error when start index is less than 0", + key: "LrangeKey7", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LRANGE", "LrangeKey7", "-1", "3"}, + expectedResponse: nil, + expectedError: errors.New("start index must be within list boundary"), + }, + { + name: "9. Error when start index is higher than the length of the list", + key: "LrangeKey8", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LRANGE", "LrangeKey8", "10", "11"}, + expectedResponse: nil, + expectedError: errors.New("start index must be within list boundary"), + }, + { + name: "10. Return error when start index is not an integer", + key: "LrangeKey9", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LRANGE", "LrangeKey9", "start", "7"}, + expectedResponse: nil, + expectedError: errors.New("start and end indices must be integers"), + }, + { + name: "11. Return error when end index is not an integer", + key: "LrangeKey10", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LRANGE", "LrangeKey10", "0", "end"}, + expectedResponse: nil, + expectedError: errors.New("start and end indices must be integers"), + }, + { + name: "12. Error when start and end indices are equal", + key: "LrangeKey11", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LRANGE", "LrangeKey11", "1", "1"}, + expectedResponse: nil, + expectedError: errors.New("start and end indices cannot be equal"), + }, + } -func Test_HandleLINDEX(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse string - expectedError error - }{ - { - name: "1. Return last element within range", - key: "LindexKey1", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"LINDEX", "LindexKey1", "3"}, - expectedResponse: "value4", - expectedError: nil, - }, - { - name: "2. Return first element within range", - key: "LindexKey2", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"LINDEX", "LindexKey1", "0"}, - expectedResponse: "value1", - expectedError: nil, - }, - { - name: "3. Return middle element within range", - key: "LindexKey3", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"LINDEX", "LindexKey1", "1"}, - expectedResponse: "value2", - expectedError: nil, - }, - { - name: "4. If key does not exist, return error", - key: "LindexKey4", - presetValue: nil, - command: []string{"LINDEX", "LindexKey4", "0"}, - expectedResponse: "", - expectedError: errors.New("LINDEX command on non-list item"), - }, - { - name: "5. Command too short", - key: "LindexKey3", - presetValue: nil, - command: []string{"LINDEX", "LindexKey3"}, - expectedResponse: "", - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: " 6. Command too long", - key: "LindexKey4", - presetValue: nil, - command: []string{"LINDEX", "LindexKey4", "0", "20"}, - expectedResponse: "", - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "7. Trying to get element by index on a non-list returns error", - key: "LindexKey5", - presetValue: "Default value", - command: []string{"LINDEX", "LindexKey5", "0"}, - expectedResponse: "", - expectedError: errors.New("LINDEX command on non-list item"), - }, - { - name: "8. Trying to get index out of range index beyond last index", - key: "LindexKey6", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LINDEX", "LindexKey6", "3"}, - expectedResponse: "", - expectedError: errors.New("index must be within list range"), - }, - { - name: "9. Trying to get index out of range with negative index", - key: "LindexKey7", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LINDEX", "LindexKey7", "-1"}, - expectedResponse: "", - expectedError: errors.New("index must be within list range"), - }, - { - name: " 10. Return error when index is not an integer", - key: "LindexKey8", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LINDEX", "LindexKey8", "index"}, - expectedResponse: "", - expectedError: errors.New("index must be an integer"), - }, - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) } - expected = "ok" - case []string: - command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} - for _, element := range test.presetValue.([]string) { - command = append(command, []resp.Value{resp.StringValue(element)}...) + + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = strconv.Itoa(len(test.presetValue.([]string))) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -299,174 +512,166 @@ func Test_HandleLINDEX(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response of length %d, got length %d", len(test.expectedResponse), len(res.Array())) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } } - return - } + }) + } + }) + + t.Run("Test_HandleLSET", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedValue []string + expectedError error + }{ + { + name: "1. Return last element within range", + key: "LsetKey1", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LSET", "LsetKey1", "3", "new-value"}, + expectedValue: []string{"value1", "value2", "value3", "new-value"}, + expectedError: nil, + }, + { + name: "2. Return first element within range", + key: "LsetKey2", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LSET", "LsetKey2", "0", "new-value"}, + expectedValue: []string{"new-value", "value2", "value3", "value4"}, + expectedError: nil, + }, + { + name: "3. Return middle element within range", + key: "LsetKey3", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LSET", "LsetKey3", "1", "new-value"}, + expectedValue: []string{"value1", "new-value", "value3", "value4"}, + expectedError: nil, + }, + { + name: "4. If key does not exist, return error", + key: "LsetKey4", + presetValue: nil, + command: []string{"LSET", "LsetKey4", "0", "element"}, + expectedValue: nil, + expectedError: errors.New("LSET command on non-list item"), + }, + { + name: "5. Command too short", + key: "LsetKey5", + presetValue: nil, + command: []string{"LSET", "LsetKey5"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + key: "LsetKey6", + presetValue: nil, + command: []string{"LSET", "LsetKey6", "0", "element", "element"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "7. Trying to get element by index on a non-list returns error", + key: "LsetKey5", + presetValue: "Default value", + command: []string{"LSET", "LsetKey5", "0", "element"}, + expectedValue: nil, + expectedError: errors.New("LSET command on non-list item"), + }, + { + name: "8. Trying to get index out of range index beyond last index", + key: "LsetKey6", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LSET", "LsetKey6", "3", "element"}, + expectedValue: nil, + expectedError: errors.New("index must be within list range"), + }, + { + name: "9. Trying to get index out of range with negative index", + key: "LsetKey7", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LSET", "LsetKey7", "-1", "element"}, + expectedValue: nil, + expectedError: errors.New("index must be within list range"), + }, + { + name: "10. Return error when index is not an integer", + key: "LsetKey8", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LSET", "LsetKey8", "index", "element"}, + expectedValue: nil, + expectedError: errors.New("index must be an integer"), + }, + } - if res.String() != test.expectedResponse { - t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) - } - }) - } -} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string -func Test_HandleLRANGE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse []string - expectedError error - }{ - { - // Return sub-list within range. - // Both start and end indices are positive. - // End index is greater than start index. - name: "1. Return sub-list within range.", - key: "LrangeKey1", - presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, - command: []string{"LRANGE", "LrangeKey1", "3", "6"}, - expectedResponse: []string{"value4", "value5", "value6", "value7"}, - expectedError: nil, - }, - { - name: "2. Return sub-list from start index to the end of the list when end index is -1", - key: "LrangeKey2", - presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, - command: []string{"LRANGE", "LrangeKey2", "3", "-1"}, - expectedResponse: []string{"value4", "value5", "value6", "value7", "value8"}, - expectedError: nil, - }, - { - name: "3. Return the reversed sub-list when the end index is greater than -1 but less than start index", - key: "LrangeKey3", - presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, - command: []string{"LRANGE", "LrangeKey3", "3", "0"}, - expectedResponse: []string{"value4", "value3", "value2", "value1"}, - expectedError: nil, - }, - { - name: "4. If key does not exist, return error", - key: "LrangeKey4", - presetValue: nil, - command: []string{"LRANGE", "LrangeKey4", "0", "2"}, - expectedResponse: nil, - expectedError: errors.New("LRANGE command on non-list item"), - }, - { - name: "5. Command too short", - key: "LrangeKey5", - presetValue: nil, - command: []string{"LRANGE", "LrangeKey5"}, - expectedResponse: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - key: "LrangeKey6", - presetValue: nil, - command: []string{"LRANGE", "LrangeKey6", "0", "element", "element"}, - expectedResponse: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "7. Error when executing command on non-list command", - key: "LrangeKey5", - presetValue: "Default value", - command: []string{"LRANGE", "LrangeKey5", "0", "3"}, - expectedResponse: nil, - expectedError: errors.New("LRANGE command on non-list item"), - }, - { - name: "8. Error when start index is less than 0", - key: "LrangeKey7", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"LRANGE", "LrangeKey7", "-1", "3"}, - expectedResponse: nil, - expectedError: errors.New("start index must be within list boundary"), - }, - { - name: "9. Error when start index is higher than the length of the list", - key: "LrangeKey8", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LRANGE", "LrangeKey8", "10", "11"}, - expectedResponse: nil, - expectedError: errors.New("start index must be within list boundary"), - }, - { - name: "10. Return error when start index is not an integer", - key: "LrangeKey9", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LRANGE", "LrangeKey9", "start", "7"}, - expectedResponse: nil, - expectedError: errors.New("start and end indices must be integers"), - }, - { - name: "11. Return error when end index is not an integer", - key: "LrangeKey10", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LRANGE", "LrangeKey10", "0", "end"}, - expectedResponse: nil, - expectedError: errors.New("start and end indices must be integers"), - }, - { - name: "12. Error when start and end indices are equal", - key: "LrangeKey11", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LRANGE", "LrangeKey11", "1", "1"}, - expectedResponse: nil, - expectedError: errors.New("start and end indices cannot be equal"), - }, - } + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case []string: - command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} - for _, element := range test.presetValue.([]string) { - command = append(command, []resp.Value{resp.StringValue(element)}...) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = strconv.Itoa(len(test.presetValue.([]string))) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -477,161 +682,196 @@ func Test_HandleLRANGE(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected response OK, got \"%s\"", res.String()) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(test.key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - return - } - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response of length %d, got length %d", len(test.expectedResponse), len(res.Array())) - } + if len(res.Array()) != len(test.expectedValue) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + test.key, len(test.expectedValue), len(res.Array())) + } - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.String()) { - t.Errorf("unexpected element \"%s\" in response", item.String()) + for _, item := range res.Array() { + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) + } } - } - }) - } -} + }) + } + }) + + t.Run("Test_HandleLTRIM", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedValue []string + expectedError error + }{ + { + // Return trim within range. + // Both start and end indices are positive. + // End index is greater than start index. + name: "1. Return trim within range.", + key: "LtrimKey1", + presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, + command: []string{"LTRIM", "LtrimKey1", "3", "6"}, + expectedValue: []string{"value4", "value5", "value6"}, + expectedError: nil, + }, + { + name: "2. Return element from start index to end index when end index is greater than length of the list", + key: "LtrimKey2", + presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, + command: []string{"LTRIM", "LtrimKey2", "5", "-1"}, + expectedValue: []string{"value6", "value7", "value8"}, + expectedError: nil, + }, + { + name: "3. Return error when end index is smaller than start index but greater than -1", + key: "LtrimKey3", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LTRIM", "LtrimKey3", "3", "1"}, + expectedValue: nil, + expectedError: errors.New("end index must be greater than start index or -1"), + }, + { + name: "4. If key does not exist, return error", + key: "LtrimKey4", + presetValue: nil, + command: []string{"LTRIM", "LtrimKey4", "0", "2"}, + expectedValue: nil, + expectedError: errors.New("LTRIM command on non-list item"), + }, + { + name: "5. Command too short", + key: "LtrimKey5", + presetValue: nil, + command: []string{"LTRIM", "LtrimKey5"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + key: "LtrimKey6", + presetValue: nil, + command: []string{"LTRIM", "LtrimKey6", "0", "element", "element"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "7. Trying to get element by index on a non-list returns error", + key: "LtrimKey5", + presetValue: "Default value", + command: []string{"LTRIM", "LtrimKey5", "0", "3"}, + expectedValue: nil, + expectedError: errors.New("LTRIM command on non-list item"), + }, + { + name: "8. Error when start index is less than 0", + key: "LtrimKey7", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LTRIM", "LtrimKey7", "-1", "3"}, + expectedValue: nil, + expectedError: errors.New("start index must be within list boundary"), + }, + { + name: "9. Error when start index is higher than the length of the list", + key: "LtrimKey8", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LTRIM", "LtrimKey8", "10", "11"}, + expectedValue: nil, + expectedError: errors.New("start index must be within list boundary"), + }, + { + name: "10. Return error when start index is not an integer", + key: "LtrimKey9", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LTRIM", "LtrimKey9", "start", "7"}, + expectedValue: nil, + expectedError: errors.New("start and end indices must be integers"), + }, + { + name: "11. Return error when end index is not an integer", + key: "LtrimKey10", + presetValue: []string{"value1", "value2", "value3"}, + command: []string{"LTRIM", "LtrimKey10", "0", "end"}, + expectedValue: nil, + expectedError: errors.New("start and end indices must be integers"), + }, + } -func Test_HandleLSET(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedValue []string - expectedError error - }{ - { - name: "1. Return last element within range", - key: "LsetKey1", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"LSET", "LsetKey1", "3", "new-value"}, - expectedValue: []string{"value1", "value2", "value3", "new-value"}, - expectedError: nil, - }, - { - name: "2. Return first element within range", - key: "LsetKey2", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"LSET", "LsetKey2", "0", "new-value"}, - expectedValue: []string{"new-value", "value2", "value3", "value4"}, - expectedError: nil, - }, - { - name: "3. Return middle element within range", - key: "LsetKey3", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"LSET", "LsetKey3", "1", "new-value"}, - expectedValue: []string{"value1", "new-value", "value3", "value4"}, - expectedError: nil, - }, - { - name: "4. If key does not exist, return error", - key: "LsetKey4", - presetValue: nil, - command: []string{"LSET", "LsetKey4", "0", "element"}, - expectedValue: nil, - expectedError: errors.New("LSET command on non-list item"), - }, - { - name: "5. Command too short", - key: "LsetKey5", - presetValue: nil, - command: []string{"LSET", "LsetKey5"}, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - key: "LsetKey6", - presetValue: nil, - command: []string{"LSET", "LsetKey6", "0", "element", "element"}, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "7. Trying to get element by index on a non-list returns error", - key: "LsetKey5", - presetValue: "Default value", - command: []string{"LSET", "LsetKey5", "0", "element"}, - expectedValue: nil, - expectedError: errors.New("LSET command on non-list item"), - }, - { - name: "8. Trying to get index out of range index beyond last index", - key: "LsetKey6", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LSET", "LsetKey6", "3", "element"}, - expectedValue: nil, - expectedError: errors.New("index must be within list range"), - }, - { - name: "9. Trying to get index out of range with negative index", - key: "LsetKey7", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LSET", "LsetKey7", "-1", "element"}, - expectedValue: nil, - expectedError: errors.New("index must be within list range"), - }, - { - name: "10. Return error when index is not an integer", - key: "LsetKey8", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LSET", "LsetKey8", "index", "element"}, - expectedValue: nil, - expectedError: errors.New("index must be an integer"), - }, - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) } - expected = "ok" - case []string: - command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} - for _, element := range test.presetValue.([]string) { - command = append(command, []resp.Value{resp.StringValue(element)}...) + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -642,191 +882,161 @@ func Test_HandleLSET(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected response OK, got \"%s\"", res.String()) - } - - if err = client.WriteArray([]resp.Value{ - resp.StringValue("LRANGE"), - resp.StringValue(test.key), - resp.StringValue("0"), - resp.StringValue("-1"), - }); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != len(test.expectedValue) { - t.Errorf("expected list at key \"%s\" to be length %d, got %d", - test.key, len(test.expectedValue), len(res.Array())) - } - - for _, item := range res.Array() { - if !slices.Contains(test.expectedValue, item.String()) { - t.Errorf("unexpected value \"%s\" in updated list", item.String()) - } - } - }) - } -} + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } -func Test_HandleLTRIM(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedValue []string - expectedError error - }{ - { - // Return trim within range. - // Both start and end indices are positive. - // End index is greater than start index. - name: "1. Return trim within range.", - key: "LtrimKey1", - presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, - command: []string{"LTRIM", "LtrimKey1", "3", "6"}, - expectedValue: []string{"value4", "value5", "value6"}, - expectedError: nil, - }, - { - name: "2. Return element from start index to end index when end index is greater than length of the list", - key: "LtrimKey2", - presetValue: []string{"value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8"}, - command: []string{"LTRIM", "LtrimKey2", "5", "-1"}, - expectedValue: []string{"value6", "value7", "value8"}, - expectedError: nil, - }, - { - name: "3. Return error when end index is smaller than start index but greater than -1", - key: "LtrimKey3", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"LTRIM", "LtrimKey3", "3", "1"}, - expectedValue: nil, - expectedError: errors.New("end index must be greater than start index or -1"), - }, - { - name: "4. If key does not exist, return error", - key: "LtrimKey4", - presetValue: nil, - command: []string{"LTRIM", "LtrimKey4", "0", "2"}, - expectedValue: nil, - expectedError: errors.New("LTRIM command on non-list item"), - }, - { - name: "5. Command too short", - key: "LtrimKey5", - presetValue: nil, - command: []string{"LTRIM", "LtrimKey5"}, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - key: "LtrimKey6", - presetValue: nil, - command: []string{"LTRIM", "LtrimKey6", "0", "element", "element"}, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "7. Trying to get element by index on a non-list returns error", - key: "LtrimKey5", - presetValue: "Default value", - command: []string{"LTRIM", "LtrimKey5", "0", "3"}, - expectedValue: nil, - expectedError: errors.New("LTRIM command on non-list item"), - }, - { - name: "8. Error when start index is less than 0", - key: "LtrimKey7", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"LTRIM", "LtrimKey7", "-1", "3"}, - expectedValue: nil, - expectedError: errors.New("start index must be within list boundary"), - }, - { - name: "9. Error when start index is higher than the length of the list", - key: "LtrimKey8", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LTRIM", "LtrimKey8", "10", "11"}, - expectedValue: nil, - expectedError: errors.New("start index must be within list boundary"), - }, - { - name: "10. Return error when start index is not an integer", - key: "LtrimKey9", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LTRIM", "LtrimKey9", "start", "7"}, - expectedValue: nil, - expectedError: errors.New("start and end indices must be integers"), - }, - { - name: "11. Return error when end index is not an integer", - key: "LtrimKey10", - presetValue: []string{"value1", "value2", "value3"}, - command: []string{"LTRIM", "LtrimKey10", "0", "end"}, - expectedValue: nil, - expectedError: errors.New("start and end indices must be integers"), - }, - } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected response OK, got \"%s\"", res.String()) + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(test.key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { + t.Error(err) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != len(test.expectedValue) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + test.key, len(test.expectedValue), len(res.Array())) + } + + for _, item := range res.Array() { + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) + } + } + }) + } + }) + + t.Run("Test_HandleLREM", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedValue []string + expectedError error + }{ + { + name: "1. Remove the first 3 elements that appear in the list", + key: "LremKey1", + presetValue: []string{"1", "2", "4", "4", "5", "6", "7", "4", "8", "4", "9", "10", "5", "4"}, + command: []string{"LREM", "LremKey1", "3", "4"}, + expectedValue: []string{"1", "2", "5", "6", "7", "8", "4", "9", "10", "5", "4"}, + expectedError: nil, + }, + { + name: "2. Remove the last 3 elements that appear in the list", + key: "LremKey2", + presetValue: []string{"1", "2", "4", "4", "5", "6", "7", "4", "8", "4", "9", "10", "5", "4"}, + command: []string{"LREM", "LremKey2", "-3", "4"}, + expectedValue: []string{"1", "2", "4", "4", "5", "6", "7", "8", "9", "10", "5"}, + expectedError: nil, + }, + { + name: "3. Command too short", + key: "LremKey3", + presetValue: nil, + command: []string{"LREM", "LremKey3"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "4. Command too long", + key: "LremKey4", + presetValue: nil, + command: []string{"LREM", "LremKey4", "0", "element", "element"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Throw error when count is not an integer", + key: "LremKey5", + presetValue: nil, + command: []string{"LREM", "LremKey5", "count", "value1"}, + expectedValue: nil, + expectedError: errors.New("count must be an integer"), + }, + { + name: "6. Throw error on non-list item", + key: "LremKey6", + presetValue: "Default value", + command: []string{"LREM", "LremKey6", "0", "value1"}, + expectedValue: nil, + expectedError: errors.New("LREM command on non-list item"), + }, + { + name: "7. Throw error on non-existent item", + key: "LremKey7", + presetValue: "Default value", + command: []string{"LREM", "LremKey7", "0", "value1"}, + expectedValue: nil, + expectedError: errors.New("LREM command on non-list item"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = "ok" - case []string: - command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} - for _, element := range test.presetValue.([]string) { - command = append(command, []resp.Value{resp.StringValue(element)}...) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -837,156 +1047,225 @@ func Test_HandleLTRIM(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected response OK, got \"%s\"", res.String()) - } - - if err = client.WriteArray([]resp.Value{ - resp.StringValue("LRANGE"), - resp.StringValue(test.key), - resp.StringValue("0"), - resp.StringValue("-1"), - }); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != len(test.expectedValue) { - t.Errorf("expected list at key \"%s\" to be length %d, got %d", - test.key, len(test.expectedValue), len(res.Array())) - } - - for _, item := range res.Array() { - if !slices.Contains(test.expectedValue, item.String()) { - t.Errorf("unexpected value \"%s\" in updated list", item.String()) - } - } - }) - } -} + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } -func Test_HandleLREM(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedValue []string - expectedError error - }{ - { - name: "1. Remove the first 3 elements that appear in the list", - key: "LremKey1", - presetValue: []string{"1", "2", "4", "4", "5", "6", "7", "4", "8", "4", "9", "10", "5", "4"}, - command: []string{"LREM", "LremKey1", "3", "4"}, - expectedValue: []string{"1", "2", "5", "6", "7", "8", "4", "9", "10", "5", "4"}, - expectedError: nil, - }, - { - name: "2. Remove the last 3 elements that appear in the list", - key: "LremKey2", - presetValue: []string{"1", "2", "4", "4", "5", "6", "7", "4", "8", "4", "9", "10", "5", "4"}, - command: []string{"LREM", "LremKey2", "-3", "4"}, - expectedValue: []string{"1", "2", "4", "4", "5", "6", "7", "8", "9", "10", "5"}, - expectedError: nil, - }, - { - name: "3. Command too short", - key: "LremKey3", - presetValue: nil, - command: []string{"LREM", "LremKey3"}, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "4. Command too long", - key: "LremKey4", - presetValue: nil, - command: []string{"LREM", "LremKey4", "0", "element", "element"}, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Throw error when count is not an integer", - key: "LremKey5", - presetValue: nil, - command: []string{"LREM", "LremKey5", "count", "value1"}, - expectedValue: nil, - expectedError: errors.New("count must be an integer"), - }, - { - name: "6. Throw error on non-list item", - key: "LremKey6", - presetValue: "Default value", - command: []string{"LREM", "LremKey6", "0", "value1"}, - expectedValue: nil, - expectedError: errors.New("LREM command on non-list item"), - }, - { - name: "7. Throw error on non-existent item", - key: "LremKey7", - presetValue: "Default value", - command: []string{"LREM", "LremKey7", "0", "value1"}, - expectedValue: nil, - expectedError: errors.New("LREM command on non-list item"), - }, - } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected response OK, got \"%s\"", res.String()) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(test.key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != len(test.expectedValue) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + test.key, len(test.expectedValue), len(res.Array())) + } + + for _, item := range res.Array() { + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) } - expected = "ok" - case []string: - command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} - for _, element := range test.presetValue.([]string) { - command = append(command, []resp.Value{resp.StringValue(element)}...) + } + }) + } + }) + + t.Run("Test_HandleLMOVE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValue map[string]interface{} + command []string + expectedValue map[string][]string + expectedError error + }{ + { + name: "1. Move element from LEFT of left list to LEFT of right list", + presetValue: map[string]interface{}{ + "source1": []string{"one", "two", "three"}, + "destination1": []string{"one", "two", "three"}, + }, + command: []string{"LMOVE", "source1", "destination1", "LEFT", "LEFT"}, + expectedValue: map[string][]string{ + "source1": {"two", "three"}, + "destination1": {"one", "one", "two", "three"}, + }, + expectedError: nil, + }, + { + name: "2. Move element from LEFT of left list to RIGHT of right list", + presetValue: map[string]interface{}{ + "source2": []string{"one", "two", "three"}, + "destination2": []string{"one", "two", "three"}, + }, + command: []string{"LMOVE", "source2", "destination2", "LEFT", "RIGHT"}, + expectedValue: map[string][]string{ + "source2": {"two", "three"}, + "destination2": {"one", "two", "three", "one"}, + }, + expectedError: nil, + }, + { + name: "3. Move element from RIGHT of left list to LEFT of right list", + presetValue: map[string]interface{}{ + "source3": []string{"one", "two", "three"}, + "destination3": []string{"one", "two", "three"}, + }, + command: []string{"LMOVE", "source3", "destination3", "RIGHT", "LEFT"}, + expectedValue: map[string][]string{ + "source3": {"one", "two"}, + "destination3": {"three", "one", "two", "three"}, + }, + expectedError: nil, + }, + { + name: "4. Move element from RIGHT of left list to RIGHT of right list", + presetValue: map[string]interface{}{ + "source4": []string{"one", "two", "three"}, + "destination4": []string{"one", "two", "three"}, + }, + command: []string{"LMOVE", "source4", "destination4", "RIGHT", "RIGHT"}, + expectedValue: map[string][]string{ + "source4": {"one", "two"}, + "destination4": {"one", "two", "three", "three"}, + }, + expectedError: nil, + }, + { + name: "5. Throw error when the right list is non-existent", + presetValue: map[string]interface{}{ + "source5": []string{"one", "two", "three"}, + }, + command: []string{"LMOVE", "source5", "destination5", "LEFT", "LEFT"}, + expectedValue: nil, + expectedError: errors.New("both source and destination must be lists"), + }, + { + name: "6. Throw error when right list in not a list", + presetValue: map[string]interface{}{ + "source6": []string{"one", "two", "tree"}, + "destination6": "Default value", + }, + command: []string{"LMOVE", "source6", "destination6", "LEFT", "LEFT"}, + expectedValue: nil, + expectedError: errors.New("both source and destination must be lists"), + }, + { + name: "7. Throw error when left list is non-existent", + presetValue: map[string]interface{}{ + "destination7": []string{"one", "two", "three"}, + }, + command: []string{"LMOVE", "source7", "destination7", "LEFT", "LEFT"}, + expectedValue: nil, + expectedError: errors.New("both source and destination must be lists"), + }, + { + name: "8. Throw error when left list is not a list", + presetValue: map[string]interface{}{ + "source8": "Default value", + "destination8": []string{"one", "two", "three"}, + }, + command: []string{"LMOVE", "source8", "destination8", "LEFT", "LEFT"}, + expectedValue: nil, + expectedError: errors.New("both source and destination must be lists"), + }, + { + name: "9. Throw error when command is too short", + presetValue: map[string]interface{}{}, + command: []string{"LMOVE", "source9", "destination9"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "10. Throw error when command is too long", + presetValue: map[string]interface{}{}, + command: []string{"LMOVE", "source10", "destination10", "LEFT", "LEFT", "RIGHT"}, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "11. Throw error when WHEREFROM argument is not LEFT/RIGHT", + presetValue: map[string]interface{}{}, + command: []string{"LMOVE", "source11", "destination11", "UP", "RIGHT"}, + expectedValue: nil, + expectedError: errors.New("wherefrom and whereto arguments must be either LEFT or RIGHT"), + }, + { + name: "12. Throw error when WHERETO argument is not LEFT/RIGHT", + presetValue: map[string]interface{}{}, + command: []string{"LMOVE", "source11", "destination11", "LEFT", "DOWN"}, + expectedValue: nil, + expectedError: errors.New("wherefrom and whereto arguments must be either LEFT or RIGHT"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + for key, value := range test.presetValue { + + var command []resp.Value + var expected string + + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(key)} + for _, element := range value.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(value.([]string))) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -997,219 +1276,135 @@ func Test_HandleLREM(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected response OK, got \"%s\"", res.String()) - } - - if err = client.WriteArray([]resp.Value{ - resp.StringValue("LRANGE"), - resp.StringValue(test.key), - resp.StringValue("0"), - resp.StringValue("-1"), - }); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != len(test.expectedValue) { - t.Errorf("expected list at key \"%s\" to be length %d, got %d", - test.key, len(test.expectedValue), len(res.Array())) - } - - for _, item := range res.Array() { - if !slices.Contains(test.expectedValue, item.String()) { - t.Errorf("unexpected value \"%s\" in updated list", item.String()) - } - } - }) - } -} + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } -func Test_HandleLMOVE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValue map[string]interface{} - command []string - expectedValue map[string][]string - expectedError error - }{ - { - name: "1. Move element from LEFT of left list to LEFT of right list", - presetValue: map[string]interface{}{ - "source1": []string{"one", "two", "three"}, - "destination1": []string{"one", "two", "three"}, - }, - command: []string{"LMOVE", "source1", "destination1", "LEFT", "LEFT"}, - expectedValue: map[string][]string{ - "source1": {"two", "three"}, - "destination1": {"one", "one", "two", "three"}, - }, - expectedError: nil, - }, - { - name: "2. Move element from LEFT of left list to RIGHT of right list", - presetValue: map[string]interface{}{ - "source2": []string{"one", "two", "three"}, - "destination2": []string{"one", "two", "three"}, - }, - command: []string{"LMOVE", "source2", "destination2", "LEFT", "RIGHT"}, - expectedValue: map[string][]string{ - "source2": {"two", "three"}, - "destination2": {"one", "two", "three", "one"}, - }, - expectedError: nil, - }, - { - name: "3. Move element from RIGHT of left list to LEFT of right list", - presetValue: map[string]interface{}{ - "source3": []string{"one", "two", "three"}, - "destination3": []string{"one", "two", "three"}, - }, - command: []string{"LMOVE", "source3", "destination3", "RIGHT", "LEFT"}, - expectedValue: map[string][]string{ - "source3": {"one", "two"}, - "destination3": {"three", "one", "two", "three"}, - }, - expectedError: nil, - }, - { - name: "4. Move element from RIGHT of left list to RIGHT of right list", - presetValue: map[string]interface{}{ - "source4": []string{"one", "two", "three"}, - "destination4": []string{"one", "two", "three"}, - }, - command: []string{"LMOVE", "source4", "destination4", "RIGHT", "RIGHT"}, - expectedValue: map[string][]string{ - "source4": {"one", "two"}, - "destination4": {"one", "two", "three", "three"}, - }, - expectedError: nil, - }, - { - name: "5. Throw error when the right list is non-existent", - presetValue: map[string]interface{}{ - "source5": []string{"one", "two", "three"}, - }, - command: []string{"LMOVE", "source5", "destination5", "LEFT", "LEFT"}, - expectedValue: nil, - expectedError: errors.New("both source and destination must be lists"), - }, - { - name: "6. Throw error when right list in not a list", - presetValue: map[string]interface{}{ - "source6": []string{"one", "two", "tree"}, - "destination6": "Default value", - }, - command: []string{"LMOVE", "source6", "destination6", "LEFT", "LEFT"}, - expectedValue: nil, - expectedError: errors.New("both source and destination must be lists"), - }, - { - name: "7. Throw error when left list is non-existent", - presetValue: map[string]interface{}{ - "destination7": []string{"one", "two", "three"}, - }, - command: []string{"LMOVE", "source7", "destination7", "LEFT", "LEFT"}, - expectedValue: nil, - expectedError: errors.New("both source and destination must be lists"), - }, - { - name: "8. Throw error when left list is not a list", - presetValue: map[string]interface{}{ - "source8": "Default value", - "destination8": []string{"one", "two", "three"}, - }, - command: []string{"LMOVE", "source8", "destination8", "LEFT", "LEFT"}, - expectedValue: nil, - expectedError: errors.New("both source and destination must be lists"), - }, - { - name: "9. Throw error when command is too short", - presetValue: map[string]interface{}{}, - command: []string{"LMOVE", "source9", "destination9"}, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "10. Throw error when command is too long", - presetValue: map[string]interface{}{}, - command: []string{"LMOVE", "source10", "destination10", "LEFT", "LEFT", "RIGHT"}, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "11. Throw error when WHEREFROM argument is not LEFT/RIGHT", - presetValue: map[string]interface{}{}, - command: []string{"LMOVE", "source11", "destination11", "UP", "RIGHT"}, - expectedValue: nil, - expectedError: errors.New("wherefrom and whereto arguments must be either LEFT or RIGHT"), - }, - { - name: "12. Throw error when WHERETO argument is not LEFT/RIGHT", - presetValue: map[string]interface{}{}, - command: []string{"LMOVE", "source11", "destination11", "LEFT", "DOWN"}, - expectedValue: nil, - expectedError: errors.New("wherefrom and whereto arguments must be either LEFT or RIGHT"), - }, - } + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected response OK, got \"%s\"", res.String()) + } + + for key, list := range test.expectedValue { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != len(list) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + key, len(test.expectedValue), len(res.Array())) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - for key, value := range test.presetValue { + for _, item := range res.Array() { + if !slices.Contains(list, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list %s", item.String(), key) + } + } + } + }) + } + }) + + t.Run("Test_HandleLPUSH", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse int + expectedValue []string + expectedError error + }{ + { + name: "1. LPUSHX to existing list prepends the element to the list", + key: "LpushKey1", + presetValue: []string{"1", "2", "4", "5"}, + command: []string{"LPUSHX", "LpushKey1", "value1", "value2"}, + expectedResponse: 6, + expectedValue: []string{"value1", "value2", "1", "2", "4", "5"}, + expectedError: nil, + }, + { + name: "2. LPUSH on existing list prepends the elements to the list", + key: "LpushKey2", + presetValue: []string{"1", "2", "4", "5"}, + command: []string{"LPUSH", "LpushKey2", "value1", "value2"}, + expectedResponse: 6, + expectedValue: []string{"value1", "value2", "1", "2", "4", "5"}, + expectedError: nil, + }, + { + name: "3. LPUSH on non-existent list creates the list", + key: "LpushKey3", + presetValue: nil, + command: []string{"LPUSH", "LpushKey3", "value1", "value2"}, + expectedResponse: 2, + expectedValue: []string{"value1", "value2"}, + expectedError: nil, + }, + { + name: "4. Command too short", + key: "LpushKey5", + presetValue: nil, + command: []string{"LPUSH", "LpushKey5"}, + expectedResponse: 0, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. LPUSHX command returns error on non-existent list", + key: "LpushKey6", + presetValue: nil, + command: []string{"LPUSHX", "LpushKey7", "count", "value1"}, + expectedResponse: 0, + expectedValue: nil, + expectedError: errors.New("LPUSHX command on non-existent key"), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { var command []resp.Value var expected string - switch value.(type) { + switch test.presetValue.(type) { case string: command = []resp.Value{ resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), } expected = "ok" case []string: - command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(key)} - for _, element := range value.([]string) { + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { command = append(command, []resp.Value{resp.StringValue(element)}...) } - expected = strconv.Itoa(len(value.([]string))) + expected = strconv.Itoa(len(test.presetValue.([]string))) } if err = client.WriteArray(command); err != nil { @@ -1224,36 +1419,34 @@ func Test_HandleLMOVE(t *testing.T) { t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - return - } - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected response OK, got \"%s\"", res.String()) - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } - for key, list := range test.expectedValue { if err = client.WriteArray([]resp.Value{ resp.StringValue("LRANGE"), - resp.StringValue(key), + resp.StringValue(test.key), resp.StringValue("0"), resp.StringValue("-1"), }); err != nil { @@ -1265,104 +1458,126 @@ func Test_HandleLMOVE(t *testing.T) { t.Error(err) } - if len(res.Array()) != len(list) { + if len(res.Array()) != len(test.expectedValue) { t.Errorf("expected list at key \"%s\" to be length %d, got %d", - key, len(test.expectedValue), len(res.Array())) + test.key, len(test.expectedValue), len(res.Array())) } for _, item := range res.Array() { - if !slices.Contains(list, item.String()) { - t.Errorf("unexpected value \"%s\" in updated list %s", item.String(), key) + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) } } - } - }) - } -} + }) + } + }) + + t.Run("Test_HandleRPUSH", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse int + expectedValue []string + expectedError error + }{ + { + name: "1. RPUSHX to existing list prepends the element to the list", + key: "RpushKey1", + presetValue: []string{"1", "2", "4", "5"}, + command: []string{"RPUSHX", "RpushKey1", "value1", "value2"}, + expectedResponse: 6, + expectedValue: []string{"1", "2", "4", "5", "value1", "value2"}, + expectedError: nil, + }, + { + name: "2. RPUSH on existing list prepends the elements to the list", + key: "RpushKey2", + presetValue: []string{"1", "2", "4", "5"}, + command: []string{"RPUSH", "RpushKey2", "value1", "value2"}, + expectedResponse: 6, + expectedValue: []string{"1", "2", "4", "5", "value1", "value2"}, + expectedError: nil, + }, + { + name: "3. RPUSH on non-existent list creates the list", + key: "RpushKey3", + presetValue: nil, + command: []string{"RPUSH", "RpushKey3", "value1", "value2"}, + expectedResponse: 2, + expectedValue: []string{"value1", "value2"}, + expectedError: nil, + }, + { + name: "4. Command too short", + key: "RpushKey5", + presetValue: nil, + command: []string{"RPUSH", "RpushKey5"}, + expectedResponse: 0, + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. RPUSHX command returns error on non-existent list", + key: "RpushKey6", + presetValue: nil, + command: []string{"RPUSHX", "RpushKey7", "count", "value1"}, + expectedResponse: 0, + expectedValue: nil, + expectedError: errors.New("RPUSHX command on non-existent key"), + }, + } -func Test_HandleLPUSH(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse int - expectedValue []string - expectedError error - }{ - { - name: "1. LPUSHX to existing list prepends the element to the list", - key: "LpushKey1", - presetValue: []string{"1", "2", "4", "5"}, - command: []string{"LPUSHX", "LpushKey1", "value1", "value2"}, - expectedResponse: 6, - expectedValue: []string{"value1", "value2", "1", "2", "4", "5"}, - expectedError: nil, - }, - { - name: "2. LPUSH on existing list prepends the elements to the list", - key: "LpushKey2", - presetValue: []string{"1", "2", "4", "5"}, - command: []string{"LPUSH", "LpushKey2", "value1", "value2"}, - expectedResponse: 6, - expectedValue: []string{"value1", "value2", "1", "2", "4", "5"}, - expectedError: nil, - }, - { - name: "3. LPUSH on non-existent list creates the list", - key: "LpushKey3", - presetValue: nil, - command: []string{"LPUSH", "LpushKey3", "value1", "value2"}, - expectedResponse: 2, - expectedValue: []string{"value1", "value2"}, - expectedError: nil, - }, - { - name: "4. Command too short", - key: "LpushKey5", - presetValue: nil, - command: []string{"LPUSH", "LpushKey5"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. LPUSHX command returns error on non-existent list", - key: "LpushKey6", - presetValue: nil, - command: []string{"LPUSHX", "LpushKey7", "count", "value1"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("LPUSHX command on non-existent key"), - }, - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = "ok" - case []string: - command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} - for _, element := range test.presetValue.([]string) { - command = append(command, []resp.Value{resp.StringValue(element)}...) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(len(test.presetValue.([]string))) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -1373,305 +1588,160 @@ func Test_HandleLPUSH(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - - if err = client.WriteArray([]resp.Value{ - resp.StringValue("LRANGE"), - resp.StringValue(test.key), - resp.StringValue("0"), - resp.StringValue("-1"), - }); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != len(test.expectedValue) { - t.Errorf("expected list at key \"%s\" to be length %d, got %d", - test.key, len(test.expectedValue), len(res.Array())) - } - - for _, item := range res.Array() { - if !slices.Contains(test.expectedValue, item.String()) { - t.Errorf("unexpected value \"%s\" in updated list", item.String()) - } - } - }) - } -} - -func Test_HandleRPUSH(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse int - expectedValue []string - expectedError error - }{ - { - name: "1. RPUSHX to existing list prepends the element to the list", - key: "RpushKey1", - presetValue: []string{"1", "2", "4", "5"}, - command: []string{"RPUSHX", "RpushKey1", "value1", "value2"}, - expectedResponse: 6, - expectedValue: []string{"1", "2", "4", "5", "value1", "value2"}, - expectedError: nil, - }, - { - name: "2. RPUSH on existing list prepends the elements to the list", - key: "RpushKey2", - presetValue: []string{"1", "2", "4", "5"}, - command: []string{"RPUSH", "RpushKey2", "value1", "value2"}, - expectedResponse: 6, - expectedValue: []string{"1", "2", "4", "5", "value1", "value2"}, - expectedError: nil, - }, - { - name: "3. RPUSH on non-existent list creates the list", - key: "RpushKey3", - presetValue: nil, - command: []string{"RPUSH", "RpushKey3", "value1", "value2"}, - expectedResponse: 2, - expectedValue: []string{"value1", "value2"}, - expectedError: nil, - }, - { - name: "4. Command too short", - key: "RpushKey5", - presetValue: nil, - command: []string{"RPUSH", "RpushKey5"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. RPUSHX command returns error on non-existent list", - key: "RpushKey6", - presetValue: nil, - command: []string{"RPUSHX", "RpushKey7", "count", "value1"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: errors.New("RPUSHX command on non-existent key"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } - expected = "ok" - case []string: - command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} - for _, element := range test.presetValue.([]string) { - command = append(command, []resp.Value{resp.StringValue(element)}...) - } - expected = strconv.Itoa(len(test.presetValue.([]string))) + return } - if err = client.WriteArray(command); err != nil { + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(test.key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - - if err = client.WriteArray([]resp.Value{ - resp.StringValue("LRANGE"), - resp.StringValue(test.key), - resp.StringValue("0"), - resp.StringValue("-1"), - }); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != len(test.expectedValue) { - t.Errorf("expected list at key \"%s\" to be length %d, got %d", - test.key, len(test.expectedValue), len(res.Array())) - } - - for _, item := range res.Array() { - if !slices.Contains(test.expectedValue, item.String()) { - t.Errorf("unexpected value \"%s\" in updated list", item.String()) - } - } - }) - } -} + if len(res.Array()) != len(test.expectedValue) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + test.key, len(test.expectedValue), len(res.Array())) + } -func Test_HandlePOP(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse string - expectedValue []string - expectedError error - }{ - { - name: "1. LPOP returns last element and removed first element from the list", - key: "PopKey1", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"LPOP", "PopKey1"}, - expectedResponse: "value1", - expectedValue: []string{"value2", "value3", "value4"}, - expectedError: nil, - }, - { - name: "2. RPOP returns last element and removed last element from the list", - key: "PopKey2", - presetValue: []string{"value1", "value2", "value3", "value4"}, - command: []string{"RPOP", "PopKey2"}, - expectedResponse: "value4", - expectedValue: []string{"value1", "value2", "value3"}, - expectedError: nil, - }, - { - name: "3. Command too short", - key: "PopKey3", - presetValue: nil, - command: []string{"LPOP"}, - expectedResponse: "", - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "4. Command too long", - key: "PopKey4", - presetValue: nil, - command: []string{"LPOP", "PopKey4", "PopKey4"}, - expectedResponse: "", - expectedValue: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Trying to execute LPOP from a non-list item return an error", - key: "PopKey5", - presetValue: "Default value", - command: []string{"LPOP", "PopKey5"}, - expectedResponse: "", - expectedValue: nil, - expectedError: errors.New("LPOP command on non-list item"), - }, - { - name: "6. Trying to execute RPOP from a non-list item return an error", - key: "PopKey6", - presetValue: "Default value", - command: []string{"RPOP", "PopKey6"}, - expectedResponse: "", - expectedValue: nil, - expectedError: errors.New("RPOP command on non-list item"), - }, - } + for _, item := range res.Array() { + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) + } + } + }) + } + }) + + t.Run("Test_HandlePOP", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse string + expectedValue []string + expectedError error + }{ + { + name: "1. LPOP returns last element and removed first element from the list", + key: "PopKey1", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"LPOP", "PopKey1"}, + expectedResponse: "value1", + expectedValue: []string{"value2", "value3", "value4"}, + expectedError: nil, + }, + { + name: "2. RPOP returns last element and removed last element from the list", + key: "PopKey2", + presetValue: []string{"value1", "value2", "value3", "value4"}, + command: []string{"RPOP", "PopKey2"}, + expectedResponse: "value4", + expectedValue: []string{"value1", "value2", "value3"}, + expectedError: nil, + }, + { + name: "3. Command too short", + key: "PopKey3", + presetValue: nil, + command: []string{"LPOP"}, + expectedResponse: "", + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "4. Command too long", + key: "PopKey4", + presetValue: nil, + command: []string{"LPOP", "PopKey4", "PopKey4"}, + expectedResponse: "", + expectedValue: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Trying to execute LPOP from a non-list item return an error", + key: "PopKey5", + presetValue: "Default value", + command: []string{"LPOP", "PopKey5"}, + expectedResponse: "", + expectedValue: nil, + expectedError: errors.New("LPOP command on non-list item"), + }, + { + name: "6. Trying to execute RPOP from a non-list item return an error", + key: "PopKey6", + presetValue: "Default value", + command: []string{"RPOP", "PopKey6"}, + expectedResponse: "", + expectedValue: nil, + expectedError: errors.New("RPOP command on non-list item"), + }, + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case []string: + command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} + for _, element := range test.presetValue.([]string) { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(len(test.presetValue.([]string))) } - expected = "ok" - case []string: - command = []resp.Value{resp.StringValue("LPUSH"), resp.StringValue(test.key)} - for _, element := range test.presetValue.([]string) { - command = append(command, []resp.Value{resp.StringValue(element)}...) + + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = strconv.Itoa(len(test.presetValue.([]string))) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -1682,59 +1752,42 @@ func Test_HandlePOP(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) - } - return - } - - if res.String() != test.expectedResponse { - t.Errorf("expected response %s, got %s", test.expectedResponse, res.String()) - } - - if err = client.WriteArray([]resp.Value{ - resp.StringValue("LRANGE"), - resp.StringValue(test.key), - resp.StringValue("0"), - resp.StringValue("-1"), - }); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != len(test.expectedValue) { - t.Errorf("expected list at key \"%s\" to be length %d, got %d", - test.key, len(test.expectedValue), len(res.Array())) - } - - for _, item := range res.Array() { - if !slices.Contains(test.expectedValue, item.String()) { - t.Errorf("unexpected value \"%s\" in updated list", item.String()) - } - } - }) - } + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } + + if res.String() != test.expectedResponse { + t.Errorf("expected response %s, got %s", test.expectedResponse, res.String()) + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("LRANGE"), + resp.StringValue(test.key), + resp.StringValue("0"), + resp.StringValue("-1"), + }); err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != len(test.expectedValue) { + t.Errorf("expected list at key \"%s\" to be length %d, got %d", + test.key, len(test.expectedValue), len(res.Array())) + } + + for _, item := range res.Array() { + if !slices.Contains(test.expectedValue, item.String()) { + t.Errorf("unexpected value \"%s\" in updated list", item.String()) + } + } + }) + } + }) } diff --git a/internal/modules/pubsub/commands_test.go b/internal/modules/pubsub/commands_test.go index b1584f3e..fbf8bf56 100644 --- a/internal/modules/pubsub/commands_test.go +++ b/internal/modules/pubsub/commands_test.go @@ -15,817 +15,950 @@ package pubsub_test import ( - "bytes" - "context" - "fmt" "github.com/echovault/echovault/echovault" "github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/constants" - "github.com/echovault/echovault/internal/modules/pubsub" "github.com/tidwall/resp" "net" - "reflect" "slices" "strings" "sync" "testing" - "time" - "unsafe" ) -var ps *pubsub.PubSub -var mockServer *echovault.EchoVault - -var bindAddr = "localhost" -var port uint16 - -func init() { - p, _ := internal.GetFreePort() - port = uint16(p) - - mockServer = setUpServer(bindAddr, port) - - getPubSub := getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getPubSub")).(func() interface{}) - ps = getPubSub().(*pubsub.PubSub) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - wg.Done() - mockServer.Start() - }() - wg.Wait() -} - -func setUpServer(bindAddr string, port uint16) *echovault.EchoVault { - server, _ := echovault.NewEchoVault( +func setUpServer(port int) (*echovault.EchoVault, error) { + return echovault.NewEchoVault( echovault.WithConfig(config.Config{ - BindAddr: bindAddr, - Port: port, + BindAddr: "localhost", + Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) - return server -} - -func getUnexportedField(field reflect.Value) interface{} { - return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() } -func getHandler(commands ...string) internal.HandlerFunc { - if len(commands) == 0 { - return nil - } - getCommands := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command) - for _, c := range getCommands() { - if strings.EqualFold(commands[0], c.Command) && len(commands) == 1 { - // Get command handler - return c.HandlerFunc - } - if strings.EqualFold(commands[0], c.Command) { - // Get sub-command handler - for _, sc := range c.SubCommands { - if strings.EqualFold(commands[1], sc.Command) { - return sc.HandlerFunc - } - } - } +func Test_PubSub(t *testing.T) { + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return } - return nil -} -func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn, mockServer *echovault.EchoVault) internal.HandlerFuncParams { - getPubSub := - getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getPubSub")).(func() interface{}) - return internal.HandlerFuncParams{ - Context: ctx, - Command: cmd, - Connection: conn, - GetPubSub: getPubSub, + mockServer, err := setUpServer(port) + if err != nil { + t.Error(err) + return } -} -func Test_HandleSubscribe(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", "SUBSCRIBE/PSUBSCRIBE") + go func() { + mockServer.Start() + }() - numOfConnection := 20 - connections := make([]*net.Conn, numOfConnection) + t.Cleanup(func() { + mockServer.ShutDown() + }) - for i := 0; i < numOfConnection; i++ { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) - if err != nil { - t.Error(err) + t.Run("Test_HandleSubscribe", func(t *testing.T) { + t.Parallel() + + // Establish connections. + numOfConnections := 20 + rawConnections := make([]net.Conn, numOfConnections) + connections := make([]*resp.Conn, numOfConnections) + for i := 0; i < numOfConnections; i++ { + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + rawConnections[i] = conn + connections[i] = resp.NewConn(conn) + } + defer func() { + for _, conn := range rawConnections { + _ = conn.Close() + } + }() + + // Test subscribe to channels + channels := []string{"sub_channel1", "sub_channel2", "sub_channel3"} + command := []resp.Value{resp.StringValue("SUBSCRIBE")} + for _, channel := range channels { + command = append(command, resp.StringValue(channel)) } - connections[i] = &conn - } - defer func() { for _, conn := range connections { - if err := (*conn).Close(); err != nil { + if err := conn.WriteArray(command); err != nil { t.Error(err) + return + } + for i := 0; i < len(channels); i++ { + // Read all the subscription confirmations from the connection. + if _, _, err := conn.ReadValue(); err != nil { + t.Error(err) + return + } } } - }() - - // Test subscribe to channels - channels := []string{"sub_channel1", "sub_channel2", "sub_channel3"} - for _, conn := range connections { - _, err := getHandler("SUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"SUBSCRIBE"}, channels...), conn, mockServer)) + activeChannels, err := mockServer.PubSubChannels("*") if err != nil { t.Error(err) + return } - } - for _, channel := range channels { - // Check if the channel exists in the pubsub module - if !slices.ContainsFunc(ps.GetAllChannels(), func(c *pubsub.Channel) bool { - return c.Name() == channel - }) { - t.Errorf("expected pubsub to contain channel \"%s\" but it was not found", channel) - } - for _, c := range ps.GetAllChannels() { - if c.Name() == channel { - // Check if channel has nil pattern - if c.Pattern() != nil { - t.Errorf("expected channel \"%s\" to have nil pattern, found pattern \"%s\"", channel, c.Name()) - } - // Check if the channel has all the connections from above - for _, conn := range connections { - if _, ok := c.Subscribers()[conn]; !ok { - t.Errorf("could not find all expected connection in the \"%s\"", channel) - } - } + numSubs, err := mockServer.PubSubNumSub(channels...) + if err != nil { + t.Error(err) + return + } + for _, channel := range channels { + // Check if the channel exists in the pubsub module. + if !slices.Contains(activeChannels, channel) { + t.Errorf("expected pubsub to contain channel \"%s\" but it was not found", channel) + return + } + // Check if the channel has the right number of subscribers. + if numSubs[channel] != len(connections) { + t.Errorf("expected channel \"%s\" to have %d subscribers, got %d", + channel, len(connections), numSubs[channel]) + return } } - } - // Test subscribe to patterns - patterns := []string{"psub_channel*"} - for _, conn := range connections { - _, err := getHandler("PSUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"PSUBSCRIBE"}, patterns...), conn, mockServer)) - if err != nil { - t.Error(err) + // Test subscribe to patterns + patterns := []string{"psub_channel*"} + command = []resp.Value{resp.StringValue("PSUBSCRIBE")} + for _, pattern := range patterns { + command = append(command, resp.StringValue(pattern)) } - } - for _, pattern := range patterns { - // Check if pattern channel exists in pubsub module - if !slices.ContainsFunc(ps.GetAllChannels(), func(c *pubsub.Channel) bool { - return c.Name() == pattern - }) { - t.Errorf("expected pubsub to contain pattern channel \"%s\" but it was not found", pattern) - } - for _, c := range ps.GetAllChannels() { - if c.Name() == pattern { - // Check if channel has non-nil pattern - if c.Pattern() == nil { - t.Errorf("expected channel \"%s\" to have pattern \"%s\", found nil pattern", pattern, c.Name()) - } - // Check if the channel has all the connections from above - for _, conn := range connections { - if _, ok := c.Subscribers()[conn]; !ok { - t.Errorf("could not find all expected connection in the \"%s\"", pattern) - } + for _, conn := range connections { + if err := conn.WriteArray(command); err != nil { + t.Error(err) + return + } + for i := 0; i < len(patterns); i++ { + // Read all the pattern subscription confirmations from the connection. + if _, _, err := conn.ReadValue(); err != nil { + t.Error(err) + return } } } - } -} - -func Test_HandleUnsubscribe(t *testing.T) { - generateConnections := func(noOfConnections int) []*net.Conn { - connections := make([]*net.Conn, noOfConnections) - for i := 0; i < noOfConnections; i++ { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) + numSubs, err = mockServer.PubSubNumSub(patterns...) + if err != nil { + t.Error(err) + return + } + for _, pattern := range patterns { + activePatterns, err := mockServer.PubSubChannels(pattern) if err != nil { t.Error(err) + return + } + // Check if pattern channel exists in pubsub module. + if !slices.Contains(activePatterns, pattern) { + t.Errorf("expected pubsub to contain pattern channel \"%s\" but it was not found", pattern) + return + } + // Check if the channel has all the connections from above. + if numSubs[pattern] != len(connections) { + t.Errorf("expected pattern channel \"%s\" to have %d subscribers, got %d", + pattern, len(connections), numSubs[pattern]) + return } - connections[i] = &conn } - return connections - } + }) - closeConnections := func(conns []*net.Conn) { - for _, conn := range conns { - if err := (*conn).Close(); err != nil { - t.Error(err) + t.Run("Test_HandleUnsubscribe", func(t *testing.T) { + t.Parallel() + + var rawConnections []net.Conn + generateConnections := func(noOfConnections int) []*resp.Conn { + connections := make([]*resp.Conn, noOfConnections) + for i := 0; i < noOfConnections; i++ { + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + } + rawConnections = append(rawConnections, conn) + connections[i] = resp.NewConn(conn) } + return connections } - } + defer func() { + for _, conn := range rawConnections { + _ = conn.Close() + } + }() - verifyResponse := func(res []byte, expectedResponse [][]string) { - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - v := rv.Array() - if len(v) != len(expectedResponse) { - t.Errorf("expected subscribe response of length %d, but got %d", len(expectedResponse), len(v)) - } - for _, item := range v { - arr := item.Array() - if len(arr) != 3 { - t.Errorf("expected subscribe response item to be length %d, but got %d", 3, len(arr)) + verifyResponse := func(res resp.Value, expectedResponse [][]string) { + v := res.Array() + if len(v) != len(expectedResponse) { + t.Errorf("expected subscribe response of length %d, but got %d", len(expectedResponse), len(v)) } - if !slices.ContainsFunc(expectedResponse, func(strings []string) bool { - return strings[0] == arr[0].String() && strings[1] == arr[1].String() && strings[2] == arr[2].String() - }) { - t.Errorf("expected to find item \"%s\" in response, did not find it.", arr[1].String()) + for _, item := range v { + arr := item.Array() + if len(arr) != 3 { + t.Errorf("expected subscribe response item to be length %d, but got %d", 3, len(arr)) + } + if !slices.ContainsFunc(expectedResponse, func(strings []string) bool { + return strings[0] == arr[0].String() && strings[1] == arr[1].String() && strings[2] == arr[2].String() + }) { + t.Errorf("expected to find item \"%s\" in response, did not find it.", arr[1].String()) + } } } - } - tests := []struct { - subChannels []string // All channels to subscribe to - subPatterns []string // All patterns to subscribe to - unSubChannels []string // Channels to unsubscribe from - unSubPatterns []string // Patterns to unsubscribe from - remainChannels []string // Channels to remain subscribed to - remainPatterns []string // Patterns to remain subscribed to - targetConn *net.Conn // Connection used to test unsubscribe functionality - otherConnections []*net.Conn // Connections to fill the subscribers list for channels and patterns - expectedResponses map[string][][]string // The expected response from the handler - }{ - { // 1. Unsubscribe from channels and patterns - subChannels: []string{"xx_channel_one", "xx_channel_two", "xx_channel_three", "xx_channel_four"}, - subPatterns: []string{"xx_pattern_[ab]", "xx_pattern_[cd]", "xx_pattern_[ef]", "xx_pattern_[gh]"}, - unSubChannels: []string{"xx_channel_one", "xx_channel_two"}, - unSubPatterns: []string{"xx_pattern_[ab]"}, - remainChannels: []string{"xx_channel_three", "xx_channel_four"}, - remainPatterns: []string{"xx_pattern_[cd]", "xx_pattern_[ef]", "xx_pattern_[gh]"}, - targetConn: generateConnections(1)[0], - otherConnections: generateConnections(20), - expectedResponses: map[string][][]string{ - "channel": { - {"unsubscribe", "xx_channel_one", "1"}, - {"unsubscribe", "xx_channel_two", "2"}, - }, - "pattern": { - {"punsubscribe", "xx_pattern_[ab]", "1"}, + tests := []struct { + subChannels []string // All channels to subscribe to + subPatterns []string // All patterns to subscribe to + unSubChannels []string // Channels to unsubscribe from + unSubPatterns []string // Patterns to unsubscribe from + remainChannels []string // Channels to remain subscribed to + remainPatterns []string // Patterns to remain subscribed to + targetConn *resp.Conn // Connection used to test unsubscribe functionality + otherConnections []*resp.Conn // Connections to fill the subscribers list for channels and patterns + expectedResponses map[string][][]string // The expected response from the handler + }{ + { // 1. Unsubscribe from channels and patterns + subChannels: []string{"xx_channel_one", "xx_channel_two", "xx_channel_three", "xx_channel_four"}, + subPatterns: []string{"xx_pattern_[ab]", "xx_pattern_[cd]", "xx_pattern_[ef]", "xx_pattern_[gh]"}, + unSubChannels: []string{"xx_channel_one", "xx_channel_two"}, + unSubPatterns: []string{"xx_pattern_[ab]"}, + remainChannels: []string{"xx_channel_three", "xx_channel_four"}, + remainPatterns: []string{"xx_pattern_[cd]", "xx_pattern_[ef]", "xx_pattern_[gh]"}, + targetConn: generateConnections(1)[0], + otherConnections: generateConnections(20), + expectedResponses: map[string][][]string{ + "channel": { + {"unsubscribe", "xx_channel_one", "1"}, + {"unsubscribe", "xx_channel_two", "2"}, + }, + "pattern": { + {"punsubscribe", "xx_pattern_[ab]", "1"}, + }, }, }, - }, - { // 2. Unsubscribe from all channels no channel or pattern is passed to command - subChannels: []string{"xx_channel_one", "xx_channel_two", "xx_channel_three", "xx_channel_four"}, - subPatterns: []string{"xx_pattern_[ab]", "xx_pattern_[cd]", "xx_pattern_[ef]", "xx_pattern_[gh]"}, - unSubChannels: []string{}, - unSubPatterns: []string{}, - remainChannels: []string{}, - remainPatterns: []string{}, - targetConn: generateConnections(1)[0], - otherConnections: generateConnections(20), - expectedResponses: map[string][][]string{ - "channel": { - {"unsubscribe", "xx_channel_one", "1"}, - {"unsubscribe", "xx_channel_two", "2"}, - {"unsubscribe", "xx_channel_three", "3"}, - {"unsubscribe", "xx_channel_four", "4"}, - }, - "pattern": { - {"punsubscribe", "xx_pattern_[ab]", "1"}, - {"punsubscribe", "xx_pattern_[cd]", "2"}, - {"punsubscribe", "xx_pattern_[ef]", "3"}, - {"punsubscribe", "xx_pattern_[gh]", "4"}, + { // 2. Unsubscribe from all channels no channel or pattern is passed to command + subChannels: []string{"xx_channel_one", "xx_channel_two", "xx_channel_three", "xx_channel_four"}, + subPatterns: []string{"xx_pattern_[ab]", "xx_pattern_[cd]", "xx_pattern_[ef]", "xx_pattern_[gh]"}, + unSubChannels: []string{}, + unSubPatterns: []string{}, + remainChannels: []string{}, + remainPatterns: []string{}, + targetConn: generateConnections(1)[0], + otherConnections: generateConnections(20), + expectedResponses: map[string][][]string{ + "channel": { + {"unsubscribe", "xx_channel_one", "1"}, + {"unsubscribe", "xx_channel_two", "2"}, + {"unsubscribe", "xx_channel_three", "3"}, + {"unsubscribe", "xx_channel_four", "4"}, + }, + "pattern": { + {"punsubscribe", "xx_pattern_[ab]", "1"}, + {"punsubscribe", "xx_pattern_[cd]", "2"}, + {"punsubscribe", "xx_pattern_[ef]", "3"}, + {"punsubscribe", "xx_pattern_[gh]", "4"}, + }, }, }, - }, - { // 3. Don't unsubscribe from any channels or patterns if the provided ones are non-existent - subChannels: []string{"xx_channel_one", "xx_channel_two", "xx_channel_three", "xx_channel_four"}, - subPatterns: []string{"xx_pattern_[ab]", "xx_pattern_[cd]", "xx_pattern_[ef]", "xx_pattern_[gh]"}, - unSubChannels: []string{"xx_channel_non_existent_channel"}, - unSubPatterns: []string{"xx_channel_non_existent_pattern_[ae]"}, - remainChannels: []string{"xx_channel_one", "xx_channel_two", "xx_channel_three", "xx_channel_four"}, - remainPatterns: []string{"xx_pattern_[ab]", "xx_pattern_[cd]", "xx_pattern_[ef]", "xx_pattern_[gh]"}, - targetConn: generateConnections(1)[0], - otherConnections: generateConnections(20), - expectedResponses: map[string][][]string{ - "channel": {}, - "pattern": {}, + { // 3. Don't unsubscribe from any channels or patterns if the provided ones are non-existent + subChannels: []string{"xx_channel_one", "xx_channel_two", "xx_channel_three", "xx_channel_four"}, + subPatterns: []string{"xx_pattern_[ab]", "xx_pattern_[cd]", "xx_pattern_[ef]", "xx_pattern_[gh]"}, + unSubChannels: []string{"xx_channel_non_existent_channel"}, + unSubPatterns: []string{"xx_channel_non_existent_pattern_[ae]"}, + remainChannels: []string{"xx_channel_one", "xx_channel_two", "xx_channel_three", "xx_channel_four"}, + remainPatterns: []string{"xx_pattern_[ab]", "xx_pattern_[cd]", "xx_pattern_[ef]", "xx_pattern_[gh]"}, + targetConn: generateConnections(1)[0], + otherConnections: generateConnections(20), + expectedResponses: map[string][][]string{ + "channel": {}, + "pattern": {}, + }, }, - }, - } + } + + for _, test := range tests { + // Subscribe to channels. + for _, conn := range append(test.otherConnections, test.targetConn) { + command := []resp.Value{resp.StringValue("SUBSCRIBE")} + for _, channel := range test.subChannels { + command = append(command, resp.StringValue(channel)) + } + if err := conn.WriteArray(command); err != nil { + t.Error(err) + return + } + for i := 0; i < len(test.subChannels); i++ { + // Read channel subscription confirmations from connection. + if _, _, err := conn.ReadValue(); err != nil { + t.Error(err) + } + } + + // Subscribe to patterns. + command = []resp.Value{resp.StringValue("PSUBSCRIBE")} + for _, pattern := range test.subPatterns { + command = append(command, resp.StringValue(pattern)) + } + if err := conn.WriteArray(command); err != nil { + t.Error(err) + return + } + for i := 0; i < len(test.subPatterns); i++ { + // Read pattern subscription confirmations from connection. + if _, _, err := conn.ReadValue(); err != nil { + t.Error(err) + } + } - for i, test := range tests { - ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("UNSUBSCRIBE/PUNSUBSCRIBE, %d", i)) + } - // Subscribe all the connections to the channels and patterns - for _, conn := range append(test.otherConnections, test.targetConn) { - _, err := getHandler("SUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"SUBSCRIBE"}, test.subChannels...), conn, mockServer)) + // Unsubscribe the target connection from the unsub channels. + command := []resp.Value{resp.StringValue("UNSUBSCRIBE")} + for _, channel := range test.unSubChannels { + command = append(command, resp.StringValue(channel)) + } + if err := test.targetConn.WriteArray(command); err != nil { + t.Error(err) + return + } + res, _, err := test.targetConn.ReadValue() if err != nil { t.Error(err) + return + } + verifyResponse(res, test.expectedResponses["channel"]) + + // Unsubscribe the target connection from the unsub patterns. + command = []resp.Value{resp.StringValue("PUNSUBSCRIBE")} + for _, pattern := range test.unSubPatterns { + command = append(command, resp.StringValue(pattern)) + } + if err = test.targetConn.WriteArray(command); err != nil { + t.Error(err) + return } - _, err = getHandler("PSUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"PSUBSCRIBE"}, test.subPatterns...), conn, mockServer)) + res, _, err = test.targetConn.ReadValue() if err != nil { t.Error(err) + return } + verifyResponse(res, test.expectedResponses["pattern"]) } + }) - // Unsubscribe the target connection from the unsub channels and patterns - res, err := getHandler("UNSUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"UNSUBSCRIBE"}, test.unSubChannels...), test.targetConn, mockServer)) - if err != nil { - t.Error(err) - } - verifyResponse(res, test.expectedResponses["channel"]) + t.Run("Test_HandlePublish", func(t *testing.T) { + t.Parallel() - res, err = getHandler("PUNSUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"PUNSUBSCRIBE"}, test.unSubPatterns...), test.targetConn, mockServer)) - if err != nil { - t.Error(err) + var rawConnections []net.Conn + establishConnection := func() *resp.Conn { + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + } + rawConnections = append(rawConnections, conn) + return resp.NewConn(conn) } - verifyResponse(res, test.expectedResponses["pattern"]) + defer func() { + for _, conn := range rawConnections { + _ = conn.Close() + } + }() - for _, channel := range append(test.unSubChannels, test.unSubPatterns...) { - for _, pubsubChannel := range ps.GetAllChannels() { - if pubsubChannel.Name() == channel { - // Assert that target connection is no longer in the unsub channels and patterns - if _, ok := pubsubChannel.Subscribers()[test.targetConn]; ok { - t.Errorf("found unexpected target connection after unsubscrining in channel \"%s\"", channel) - } - for _, conn := range test.otherConnections { - if _, ok := pubsubChannel.Subscribers()[conn]; !ok { - t.Errorf("did not find expected other connection in channel \"%s\"", channel) - } - } + // verifyChannelMessage reads the message from the connection and asserts whether + // it's the message we expect to read as a subscriber of a channel or pattern. + verifyEvent := func(client *resp.Conn, expected []string) { + rv, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + v := rv.Array() + for i := 0; i < len(v); i++ { + if v[i].String() != expected[i] { + t.Errorf("expected item at index %d to be \"%s\", got \"%s\"", i, expected[i], v[i].String()) } } } - // Assert that the target connection is still in the remain channels and patterns - for _, channel := range append(test.remainChannels, test.remainPatterns...) { - for _, pubsubChannel := range ps.GetAllChannels() { - if pubsubChannel.Name() == channel { - if _, ok := pubsubChannel.Subscribers()[test.targetConn]; !ok { - t.Errorf("could not find expected target connection in channel \"%s\"", channel) - } + // The subscribe function handles subscribing the connection to the given + // channels and patterns and reading/verifying the message sent by the server after + // subscription. + subscribe := func(client *resp.Conn, channels []string, patterns []string) { + // Subscribe to channels + command := []resp.Value{resp.StringValue("SUBSCRIBE")} + for _, channel := range channels { + command = append(command, resp.StringValue(channel)) + } + if err := client.WriteArray(command); err != nil { + t.Error(err) + } + for i := 0; i < len(channels); i++ { + // Read channel subscription confirmations. + if _, _, err := client.ReadValue(); err != nil { + t.Error(err) } } - } - } - - for _, test := range tests { - // Close all the connections - closeConnections(append(test.otherConnections, test.targetConn)) - } -} -func Test_HandlePublish(t *testing.T) { - ctx := context.WithValue(context.Background(), "test_name", "PUBLISH") - - // verifyChannelMessage reads the message from the connection and asserts whether - // it's the message we expect to read as a subscriber of a channel or pattern. - verifyEvent := func(c *net.Conn, r *resp.Conn, expected []string) { - if err := (*c).SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil { - t.Error(err) - } - rv, _, err := r.ReadValue() - if err != nil { - t.Error(err) - } - v := rv.Array() - for i := 0; i < len(v); i++ { - if v[i].String() != expected[i] { - t.Errorf("expected item at index %d to be \"%s\", got \"%s\"", i, expected[i], v[i].String()) + // Subscribe to all the patterns + command = []resp.Value{resp.StringValue("PSUBSCRIBE")} + for _, pattern := range patterns { + command = append(command, resp.StringValue(pattern)) + } + if err := client.WriteArray(command); err != nil { + t.Error(err) + } + for i := 0; i < len(patterns); i++ { + // Read pattern subscription confirmations. + if _, _, err := client.ReadValue(); err != nil { + t.Error(err) + } } } - } - // The subscribe function handles subscribing the connection to the given - // channels and patterns and reading/verifying the message sent by the echovault after - // subscription. - subscribe := func(ctx context.Context, channels []string, patterns []string, c *net.Conn, r *resp.Conn) { - // Subscribe to channels - go func() { - _, _ = getHandler("SUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"SUBSCRIBE"}, channels...), c, mockServer)) - }() - // Verify all the responses for each channel subscription - for i := 0; i < len(channels); i++ { - verifyEvent(c, r, []string{"subscribe", channels[i], fmt.Sprintf("%d", i+1)}) + subscriptions := []struct { + client *resp.Conn + channels []string + patterns []string + }{ + { + client: establishConnection(), + channels: []string{"pub_channel_1", "pub_channel_2", "pub_channel_3"}, + patterns: []string{"pub_channel_[456]"}, + }, + { + client: establishConnection(), + channels: []string{"pub_channel_6", "pub_channel_7"}, + patterns: []string{"pub_channel_[891]"}, + }, } - // Subscribe to all the patterns - go func() { - _, _ = getHandler("PSUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"PSUBSCRIBE"}, patterns...), c, mockServer)) - }() - // Verify all the responses for each pattern subscription - for i := 0; i < len(patterns); i++ { - verifyEvent(c, r, []string{"psubscribe", patterns[i], fmt.Sprintf("%d", i+1)}) + for _, subscription := range subscriptions { + // Subscribe to channels and patterns. + subscribe(subscription.client, subscription.channels, subscription.patterns) } - } - - subscriptions := map[string]map[string][]string{ - "subscriber1": { - "channels": {"pub_channel_1", "pub_channel_2", "pub_channel_3"}, // Channels to subscribe to - "patterns": {"pub_channel_[456]"}, // Patterns to subscribe to - }, - "subscriber2": { - "channels": {"pub_channel_6", "pub_channel_7"}, // Channels to subscribe to - "patterns": {"pub_channel_[891]"}, // Patterns to subscribe to - }, - } - - // Create subscriber one and subscribe to channels and patterns - r1, w1 := net.Pipe() - rc1 := resp.NewConn(r1) - subscribe(ctx, subscriptions["subscriber1"]["channels"], subscriptions["subscriber1"]["patterns"], &w1, rc1) - // Create subscriber two and subscribe to channels and patterns - r2, w2 := net.Pipe() - rc2 := resp.NewConn(r2) - subscribe(ctx, subscriptions["subscriber2"]["channels"], subscriptions["subscriber2"]["patterns"], &w2, rc2) - - type SubscriberType struct { - c *net.Conn - r *resp.Conn - l string - } + type Subscriber struct { + client *resp.Conn + channel string + } - tests := []struct { - channel string - message string - subscribers []SubscriberType - }{ - { - channel: "pub_channel_1", - message: "Test both subscribers 1", - subscribers: []SubscriberType{ - {c: &r1, r: rc1, l: "pub_channel_1"}, - {c: &r2, r: rc2, l: "pub_channel_[891]"}, + tests := []struct { + channel string + message string + subscribers []Subscriber + }{ + { + channel: "pub_channel_1", + message: "Test both subscribers 1", + subscribers: []Subscriber{ + {client: subscriptions[0].client, channel: "pub_channel_1"}, + {client: subscriptions[1].client, channel: "pub_channel_[891]"}, + }, }, - }, - { - channel: "pub_channel_6", - message: "Test both subscribers 2", - subscribers: []SubscriberType{ - {c: &r1, r: rc1, l: "pub_channel_[456]"}, - {c: &r2, r: rc2, l: "pub_channel_6"}, + { + channel: "pub_channel_6", + message: "Test both subscribers 2", + subscribers: []Subscriber{ + {client: subscriptions[0].client, channel: "pub_channel_[456]"}, + {client: subscriptions[1].client, channel: "pub_channel_6"}, + }, }, - }, - { - channel: "pub_channel_2", - message: "Test subscriber 1 1", - subscribers: []SubscriberType{ - {c: &r1, r: rc1, l: "pub_channel_2"}, + { + channel: "pub_channel_2", + message: "Test subscriber 1 1", + subscribers: []Subscriber{ + {client: subscriptions[0].client, channel: "pub_channel_2"}, + }, }, - }, - { - channel: "pub_channel_3", - message: "Test subscriber 1 2", - subscribers: []SubscriberType{ - {c: &r1, r: rc1, l: "pub_channel_3"}, + { + channel: "pub_channel_3", + message: "Test subscriber 1 2", + subscribers: []Subscriber{ + {client: subscriptions[0].client, channel: "pub_channel_3"}, + }, }, - }, - { - channel: "pub_channel_4", - message: "Test both subscribers 2", - subscribers: []SubscriberType{ - {c: &r1, r: rc1, l: "pub_channel_[456]"}, + { + channel: "pub_channel_4", + message: "Test both subscribers 2", + subscribers: []Subscriber{ + {client: subscriptions[0].client, channel: "pub_channel_[456]"}, + }, }, - }, - { - channel: "pub_channel_5", - message: "Test subscriber 1 3", - subscribers: []SubscriberType{ - {c: &r1, r: rc1, l: "pub_channel_[456]"}, + { + channel: "pub_channel_5", + message: "Test subscriber 1 3", + subscribers: []Subscriber{ + {client: subscriptions[0].client, channel: "pub_channel_[456]"}, + }, }, - }, - { - channel: "pub_channel_7", - message: "Test subscriber 2 1", - subscribers: []SubscriberType{ - {c: &r2, r: rc2, l: "pub_channel_7"}, + { + channel: "pub_channel_7", + message: "Test subscriber 2 1", + subscribers: []Subscriber{ + {client: subscriptions[1].client, channel: "pub_channel_7"}, + }, }, - }, - { - channel: "pub_channel_8", - message: "Test subscriber 2 2", - subscribers: []SubscriberType{ - {c: &r1, r: rc2, l: "pub_channel_[891]"}, + { + channel: "pub_channel_8", + message: "Test subscriber 2 2", + subscribers: []Subscriber{ + {client: subscriptions[1].client, channel: "pub_channel_[891]"}, + }, }, - }, - { - channel: "pub_channel_9", - message: "Test subscriber 2 3", - subscribers: []SubscriberType{ - {c: &r2, r: rc2, l: "pub_channel_[891]"}, + { + channel: "pub_channel_9", + message: "Test subscriber 2 3", + subscribers: []Subscriber{ + {client: subscriptions[1].client, channel: "pub_channel_[891]"}, + }, }, - }, - } - - // Dial echovault to make publisher connection - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", bindAddr, port)) - if err != nil { - t.Error(err) - } - defer func() { - if err = conn.Close(); err != nil { - t.Error(err) - } - }() - w := resp.NewConn(conn) - - for _, test := range tests { - err = w.WriteArray([]resp.Value{ - resp.StringValue("PUBLISH"), - resp.StringValue(test.channel), - resp.StringValue(test.message), - }) - if err != nil { - t.Error(err) } - rv, _, err := w.ReadValue() + // Dial echovault to make publisher connection + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) + return } - if rv.String() != "OK" { - t.Errorf("Expected publish response to be \"OK\", got \"%s\"", rv.String()) - } - - for _, sub := range test.subscribers { - verifyEvent(sub.c, sub.r, []string{"message", sub.l, test.message}) - } - } -} - -func Test_HandlePubSubChannels(t *testing.T) { - done := make(chan struct{}) - go func() { - // Create separate mock echovault for this test - port, _ := internal.GetFreePort() - mockServer := setUpServer(bindAddr, uint16(port)) - - ctx := context.WithValue(context.Background(), "test_name", "PUBSUB CHANNELS") - - channels := []string{"channel_1", "channel_2", "channel_3"} - patterns := []string{"channel_[123]", "channel_[456]"} - - rConn1, wConn1 := net.Pipe() - rc1 := resp.NewConn(rConn1) - - rConn2, wConn2 := net.Pipe() - rc2 := resp.NewConn(rConn2) + publisher := resp.NewConn(conn) - // Subscribe connections to channels - go func() { - _, err := getHandler("SUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"SUBSCRIBE"}, channels...), &wConn1, mockServer)) + for _, test := range tests { + err = publisher.WriteArray([]resp.Value{ + resp.StringValue("PUBLISH"), + resp.StringValue(test.channel), + resp.StringValue(test.message), + }) if err != nil { t.Error(err) } - }() - for i := 0; i < len(channels); i++ { - v, _, err := rc1.ReadValue() + + rv, _, err := publisher.ReadValue() if err != nil { t.Error(err) } - if !slices.ContainsFunc(channels, func(s string) bool { - return s == v.Array()[1].String() - }) { - t.Errorf("unexpected channel %s in response", v.Array()[1].String()) + if rv.String() != "OK" { + t.Errorf("Expected publish response to be \"OK\", got \"%s\"", rv.String()) + } + + for _, sub := range test.subscribers { + verifyEvent(sub.client, []string{"message", sub.channel, test.message}) } } - go func() { - _, err := getHandler("PSUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"PSUBSCRIBE"}, patterns...), &wConn2, mockServer)) - if err != nil { - t.Error(err) + }) + + t.Run("Test_HandlePubSubChannels", func(t *testing.T) { + t.Parallel() + + verifyExpectedResponse := func(res resp.Value, expected []string) { + if len(res.Array()) != len(expected) { + t.Errorf("expected response array of length %d, got %d", len(expected), len(res.Array())) } + for _, e := range expected { + if !slices.ContainsFunc(res.Array(), func(v resp.Value) bool { + return e == v.String() + }) { + t.Errorf("expected to find element \"%s\" in response array, could not find it", e) + } + } + } + + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + mockServer, err := setUpServer(port) + if err != nil { + t.Error(err) + return + } + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() }() - for i := 0; i < len(patterns); i++ { - v, _, err := rc2.ReadValue() + wg.Wait() + + subscribers := make([]*resp.Conn, 2) + for i := 0; i < len(subscribers); i++ { + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) + return } - if !slices.ContainsFunc(patterns, func(s string) bool { - return s == v.Array()[1].String() - }) { - t.Errorf("unexpected pattern %s in response", v.Array()[1].String()) - } + subscribers[i] = resp.NewConn(conn) } - verifyExpectedResponse := func(res []byte, expected []string) { - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) + channels := []string{"channel_1", "channel_2", "channel_3"} + patterns := []string{"channel_[123]", "channel_[456]"} + + subscriptions := []struct { + client *resp.Conn + action string + channels []string + patterns []string + }{ + { + client: subscribers[0], + action: "SUBSCRIBE", + channels: channels, + patterns: make([]string, 0), + }, + { + client: subscribers[1], + action: "PSUBSCRIBE", + channels: make([]string, 0), + patterns: patterns, + }, + } + for _, subscription := range subscriptions { + command := []resp.Value{resp.StringValue(subscription.action)} + if len(subscription.channels) > 0 { + for _, channel := range subscription.channels { + command = append(command, resp.StringValue(channel)) + } + } else if len(subscription.patterns) > 0 { + for _, pattern := range subscription.patterns { + command = append(command, resp.StringValue(pattern)) + } } - if len(rv.Array()) != len(expected) { - t.Errorf("expected response array of length %d, got %d", len(expected), len(rv.Array())) + if err := subscription.client.WriteArray(command); err != nil { + t.Error(err) } - for _, e := range expected { - if !slices.ContainsFunc(rv.Array(), func(v resp.Value) bool { - return e == v.String() - }) { - t.Errorf("expected to find element \"%s\" in response array, could not find it", e) + if len(subscription.channels) > 0 { + for i := 0; i < len(subscription.channels); i++ { + _, _, _ = subscription.client.ReadValue() } + return + } + for i := 0; i < len(subscription.patterns); i++ { + _, _, _ = subscription.client.ReadValue() } } - // Check if all subscriptions are returned - res, err := getHandler("PUBSUB", "CHANNELS")(getHandlerFuncParams(ctx, []string{"PUBSUB", "CHANNELS"}, nil, mockServer)) + // Get fresh connection for the next phase. + conn, err := internal.GetConnection("localhost", port) if err != nil { t.Error(err) + return } - verifyExpectedResponse(res, append(channels, patterns...)) + client := resp.NewConn(conn) - // Unsubscribe from one pattern and one channel before checking against a new slice of - // expected channels/patterns in the response of the "PUBSUB CHANNELS" command - _, err = getHandler("UNSUBSCRIBE")(getHandlerFuncParams( - ctx, - append([]string{"UNSUBSCRIBE"}, []string{"channel_2", "channel_3"}...), - &wConn1, - mockServer, - )) - if err != nil { + // Check if all subscriptions are returned. + if err = client.WriteArray([]resp.Value{resp.StringValue("PUBSUB"), resp.StringValue("CHANNELS")}); err != nil { t.Error(err) } - _, err = getHandler("UNSUBSCRIBE")(getHandlerFuncParams( - ctx, - append([]string{"UNSUBSCRIBE"}, "channel_[456]"), - &wConn2, - mockServer, - )) + res, _, err := client.ReadValue() if err != nil { t.Error(err) } + verifyExpectedResponse(res, append(channels, patterns...)) + + // Unsubscribe from one pattern and one channel before checking against a new slice of + // expected channels/patterns in the response of the "PUBSUB CHANNELS" command. + for _, unsubscribe := range []struct { + client *resp.Conn + command []resp.Value + }{ + { + client: subscribers[0], + command: []resp.Value{resp.StringValue("UNSUBSCRIBE"), resp.StringValue("channel_2"), resp.StringValue("channel_3")}, + }, + { + client: subscribers[1], + command: []resp.Value{resp.StringValue("UNSUBSCRIBE"), resp.StringValue("channel_[456]")}, + }, + } { + if err = unsubscribe.client.WriteArray(unsubscribe.command); err != nil { + t.Error(err) + } + for i := 0; i < len(unsubscribe.command[1:]); i++ { + _, _, err = unsubscribe.client.ReadValue() + if err != nil { + t.Error(err) + } + } + } - // Return all the remaining channels - res, err = getHandler("PUBSUB", "CHANNELS")(getHandlerFuncParams(ctx, []string{"PUBSUB", "CHANNELS"}, nil, mockServer)) + // Return all the remaining channels. + if err = client.WriteArray([]resp.Value{resp.StringValue("PUBSUB"), resp.StringValue("CHANNELS")}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() if err != nil { t.Error(err) } verifyExpectedResponse(res, []string{"channel_1", "channel_[123]"}) - // Return only one of the remaining channels when passed a pattern that matches it - res, err = getHandler("PUBSUB", "CHANNELS")(getHandlerFuncParams(ctx, []string{"PUBSUB", "CHANNELS", "channel_[189]"}, nil, mockServer)) - if err != nil { + + // Return only one of the remaining channels when passed a pattern that matches it. + if err = client.WriteArray([]resp.Value{ + resp.StringValue("PUBSUB"), + resp.StringValue("CHANNELS"), + resp.StringValue("channel_[189]"), + }); err != nil { t.Error(err) } verifyExpectedResponse(res, []string{"channel_1"}) - // Return both remaining channels when passed a pattern that matches them - res, err = getHandler("PUBSUB", "CHANNELS")(getHandlerFuncParams(ctx, []string{"PUBSUB", "CHANNELS", "channel_[123]"}, nil, mockServer)) + + // Return both remaining channels when passed a pattern that matches them. + if err := client.WriteArray([]resp.Value{ + resp.StringValue("PUBSUB"), + resp.StringValue("CHANNELS"), + resp.StringValue("channel_[123]"), + }); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() if err != nil { t.Error(err) } verifyExpectedResponse(res, []string{"channel_1", "channel_[123]"}) - // Return none channels when passed a pattern that does not match either channel - res, err = getHandler("PUBSUB", "CHANNELS")(getHandlerFuncParams(ctx, []string{"PUBSUB", "CHANNELS", "channel_[456]"}, nil, mockServer)) + + // Return no channels when passed a pattern that does not match either channel. + if err = client.WriteArray([]resp.Value{ + resp.StringValue("PUBSUB"), + resp.StringValue("CHANNELS"), + resp.StringValue("channel_[456]"), + }); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() if err != nil { t.Error(err) } verifyExpectedResponse(res, []string{}) + }) - done <- struct{}{} - }() + t.Run("Test_HandleNumPat", func(t *testing.T) { + t.Parallel() - select { - case <-time.After(200 * time.Millisecond): - t.Error("timeout") - case <-done: - } -} + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } -func Test_HandleNumPat(t *testing.T) { - done := make(chan struct{}) - go func() { - // Create separate mock echovault for this test - port, _ := internal.GetFreePort() - mockServer := setUpServer(bindAddr, uint16(port)) + mockServer, err := setUpServer(port) + if err != nil { + t.Error(err) + return + } + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() + }() + wg.Wait() - ctx := context.WithValue(context.Background(), "test_name", "PUBSUB NUMPAT") + // Create subscribers. + subscribers := make([]*resp.Conn, 3) + for i := 0; i < len(subscribers); i++ { + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + subscribers[i] = resp.NewConn(conn) + } patterns := []string{"pattern_[123]", "pattern_[456]", "pattern_[789]"} - connections := make([]struct { - w *net.Conn - r *resp.Conn - }, 3) - for i := 0; i < len(connections); i++ { - w, r := net.Pipe() - connections[i] = struct { - w *net.Conn - r *resp.Conn - }{w: &w, r: resp.NewConn(r)} - go func() { - _, err := getHandler("PSUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"PSUBSCRIBE"}, patterns...), &w, mockServer)) + // Subscribe to all patterns + for _, client := range subscribers { + command := []resp.Value{resp.StringValue("PSUBSCRIBE")} + for _, pattern := range patterns { + command = append(command, resp.StringValue(pattern)) + } + if err := client.WriteArray(command); err != nil { + t.Error(err) + } + // Read subscription responses to make sure we've subscribed to all the channels. + for i := 0; i < len(patterns); i++ { + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - }() - for j := 0; j < len(patterns); j++ { - v, _, err := connections[i].r.ReadValue() - if err != nil { - t.Error(err) + if len(res.Array()) != 3 { + t.Errorf("expected array response of length %d, got %d", 3, len(res.Array())) } - arr := v.Array() - if !slices.ContainsFunc(patterns, func(s string) bool { - return s == arr[1].String() - }) { - t.Errorf("found unexpected pattern in response \"%s\"", arr[1].String()) + if !strings.EqualFold(res.Array()[0].String(), "psubscribe") { + t.Errorf("expected the first array item to be \"psubscribe\", got \"%s\"", res.Array()[0].String()) + } + if !slices.Contains(patterns, res.Array()[1].String()) { + t.Errorf("unexpected channel name \"%s\", expected %v", res.Array()[1].String(), patterns) } } } - verifyNumPatResponse := func(res []byte, expected int) { - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } - if rv.Integer() != expected { - t.Errorf("expected first NUMPAT response to be %d, got %d", expected, rv.Integer()) - } + // Get fresh connection for the next phase. + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return } + client := resp.NewConn(conn) - // Check that we receive all the patterns with NUMPAT commands - res, err := getHandler("PUBSUB", "NUMPAT")(getHandlerFuncParams(ctx, []string{"PUBSUB", "NUMPAT"}, nil, mockServer)) - if err != nil { + // Check that we receive all the patterns with NUMPAT commands. + if err = client.WriteArray([]resp.Value{resp.StringValue("PUBSUB"), resp.StringValue("NUMPAT")}); err != nil { t.Error(err) } - verifyNumPatResponse(res, len(patterns)) + res, _, err := client.ReadValue() + if res.Integer() != len(patterns) { + t.Errorf("expected response \"%d\", got \"%d\"", len(patterns), res.Integer()) + } - // Unsubscribe from a channel and check if the number of active channels is updated - for _, conn := range connections { - _, err = getHandler("PUNSUBSCRIBE")(getHandlerFuncParams(ctx, []string{"PUNSUBSCRIBE", patterns[0]}, conn.w, mockServer)) + // Unsubscribe all subscribers from one pattern and check if the response is updated. + for _, subscriber := range subscribers { + if err = subscriber.WriteArray([]resp.Value{ + resp.StringValue("PUNSUBSCRIBE"), + resp.StringValue(patterns[0]), + }); err != nil { + t.Error(err) + } + res, _, err = subscriber.ReadValue() if err != nil { t.Error(err) } + if len(res.Array()[0].Array()) != 3 { + t.Errorf("expected array response of length %d, got %d", 3, len(res.Array()[0].Array())) + } + if !strings.EqualFold(res.Array()[0].Array()[0].String(), "punsubscribe") { + t.Errorf("expected the first array item to be \"punsubscribe\", got \"%s\"", res.Array()[0].Array()[0].String()) + } + if res.Array()[0].Array()[1].String() != patterns[0] { + t.Errorf("unexpected channel name \"%s\", expected %s", res.Array()[0].Array()[1].String(), patterns[0]) + } } - res, err = getHandler("PUBSUB", "NUMPAT")(getHandlerFuncParams(ctx, []string{"PUBSUB", "NUMPAT"}, nil, mockServer)) - if err != nil { + if err = client.WriteArray([]resp.Value{resp.StringValue("PUBSUB"), resp.StringValue("NUMPAT")}); err != nil { t.Error(err) } - verifyNumPatResponse(res, len(patterns)-1) + res, _, err = client.ReadValue() + if res.Integer() != len(patterns)-1 { + t.Errorf("expected response \"%d\", got \"%d\"", len(patterns)-1, res.Integer()) + } // Unsubscribe from all the channels and check if we get a 0 response - for _, conn := range connections { - _, err = getHandler("PUNSUBSCRIBE")(getHandlerFuncParams(ctx, []string{"PUNSUBSCRIBE"}, conn.w, mockServer)) - if err != nil { - t.Error(err) + for _, subscriber := range subscribers { + for _, pattern := range patterns[1:] { + if err = subscriber.WriteArray([]resp.Value{ + resp.StringValue("PUNSUBSCRIBE"), + resp.StringValue(pattern), + }); err != nil { + t.Error(err) + } + res, _, err = subscriber.ReadValue() + if err != nil { + t.Error(err) + } + if len(res.Array()[0].Array()) != 3 { + t.Errorf("expected array response of length %d, got %d", 3, + len(res.Array()[0].Array())) + } + if !strings.EqualFold(res.Array()[0].Array()[0].String(), "punsubscribe") { + t.Errorf("expected the first array item to be \"punsubscribe\", got \"%s\"", + res.Array()[0].Array()[0].String()) + } + if res.Array()[0].Array()[1].String() != pattern { + t.Errorf("unexpected channel name \"%s\", expected %s", + res.Array()[0].Array()[1].String(), pattern) + } } } - res, err = getHandler("PUBSUB", "NUMPAT")(getHandlerFuncParams(ctx, []string{"PUBSUB", "NUMPAT"}, nil, mockServer)) - if err != nil { + if err = client.WriteArray([]resp.Value{resp.StringValue("PUBSUB"), resp.StringValue("NUMPAT")}); err != nil { t.Error(err) } - verifyNumPatResponse(res, 0) + res, _, err = client.ReadValue() + if res.Integer() != 0 { + t.Errorf("expected response \"%d\", got \"%d\"", 0, res.Integer()) + } + }) - done <- struct{}{} - }() + t.Run("Test_HandleNumSub", func(t *testing.T) { + t.Parallel() - select { - case <-time.After(200 * time.Millisecond): - t.Error("timeout") - case <-done: - } -} + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } -func Test_HandleNumSub(t *testing.T) { - done := make(chan struct{}) - go func() { - // Create separate mock echovault for this test - port, _ := internal.GetFreePort() - mockServer := setUpServer(bindAddr, uint16(port)) + mockServer, err := setUpServer(port) + if err != nil { + t.Error(err) + return + } - ctx := context.WithValue(context.Background(), "test_name", "PUBSUB NUMSUB") + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() + }() + wg.Wait() channels := []string{"channel_1", "channel_2", "channel_3"} - connections := make([]struct { - w *net.Conn - r *resp.Conn - }, 3) - for i := 0; i < len(connections); i++ { - w, r := net.Pipe() - connections[i] = struct { - w *net.Conn - r *resp.Conn - }{w: &w, r: resp.NewConn(r)} - go func() { - _, err := getHandler("SUBSCRIBE")(getHandlerFuncParams(ctx, append([]string{"SUBSCRIBE"}, channels...), &w, mockServer)) + + for i := 0; i < 3; i++ { + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + client := resp.NewConn(conn) + command := []resp.Value{ + resp.StringValue("SUBSCRIBE"), + } + for _, channel := range channels { + command = append(command, resp.StringValue(channel)) + } + err = client.WriteArray(command) + if err != nil { + t.Error(err) + } + + // Read subscription responses to make sure we've subscribed to all the channels. + for i := 0; i < len(channels); i++ { + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - }() - for j := 0; j < len(channels); j++ { - v, _, err := connections[i].r.ReadValue() - if err != nil { - t.Error(err) + if len(res.Array()) != 3 { + t.Errorf("expected array response of length %d, got %d", 3, len(res.Array())) } - arr := v.Array() - if !slices.ContainsFunc(channels, func(s string) bool { - return s == arr[1].String() - }) { - t.Errorf("found unexpected pattern in response \"%s\"", arr[1].String()) + if !strings.EqualFold(res.Array()[0].String(), "subscribe") { + t.Errorf("expected the first array item to be \"subscribe\", got \"%s\"", res.Array()[0].String()) + } + if !slices.Contains(channels, res.Array()[1].String()) { + t.Errorf("unexpected channel name \"%s\", expected %v", res.Array()[1].String(), channels) } } } + // Get fresh connection for the next phase. + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + client := resp.NewConn(conn) + tests := []struct { + name string cmd []string expectedResponse [][]string }{ - { // 1. Get all subscriptions on existing channels + { + name: "1. Get all subscriptions on existing channels", cmd: append([]string{"PUBSUB", "NUMSUB"}, channels...), expectedResponse: [][]string{{"channel_1", "3"}, {"channel_2", "3"}, {"channel_3", "3"}}, }, - { // 2. Get all the subscriptions of on existing channels and a few non-existent ones - cmd: append([]string{"PUBSUB", "NUMSUB", "non_existent_channel_1", "non_existent_channel_2"}, channels...), + { + name: "2. Get all the subscriptions of on existing channels and a few non-existent ones", + cmd: append([]string{"PUBSUB", "NUMSUB", "non_existent_channel_1", "non_existent_channel_2"}, channels...), expectedResponse: [][]string{ {"non_existent_channel_1", "0"}, {"non_existent_channel_2", "0"}, @@ -834,51 +967,47 @@ func Test_HandleNumSub(t *testing.T) { {"channel_3", "3"}, }, }, - { // 3. Get an empty array when channels are not provided in the command + { + name: "3. Get an empty array when channels are not provided in the command", cmd: []string{"PUBSUB", "NUMSUB"}, expectedResponse: make([][]string, 0), }, } - for i, test := range tests { - ctx = context.WithValue(ctx, "test_index", i) - - res, err := getHandler("PUBSUB", "NUMSUB")(getHandlerFuncParams(ctx, test.cmd, nil, mockServer)) - if err != nil { - t.Error(err) - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var command []resp.Value + for _, token := range test.cmd { + command = append(command, resp.StringValue(token)) + } - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } - arr := rv.Array() - if len(arr) != len(test.expectedResponse) { - t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(arr)) - } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - for _, item := range arr { - itemArr := item.Array() - if len(itemArr) != 2 { - t.Errorf("expected each response item to be of length 2, got %d", len(itemArr)) + arr := res.Array() + if len(arr) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(arr)) } - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - return expected[0] == itemArr[0].String() && expected[1] == itemArr[1].String() - }) { - t.Errorf("could not find entry with channel \"%s\", with %d subscribers in expected response", - itemArr[0].String(), itemArr[1].Integer()) + + for _, item := range arr { + itemArr := item.Array() + if len(itemArr) != 2 { + t.Errorf("expected each response item to be of length 2, got %d", len(itemArr)) + } + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == itemArr[0].String() && expected[1] == itemArr[1].String() + }) { + t.Errorf("could not find entry with channel \"%s\", with %d subscribers in expected response", + itemArr[0].String(), itemArr[1].Integer()) + } } - } + }) } - - done <- struct{}{} - }() - - select { - case <-time.After(200 * time.Millisecond): - t.Error("timeout") - case <-done: - } + }) } diff --git a/internal/modules/set/commands_test.go b/internal/modules/set/commands_test.go index bbc80db6..dd9af2c0 100644 --- a/internal/modules/set/commands_test.go +++ b/internal/modules/set/commands_test.go @@ -16,121 +16,146 @@ package set_test import ( "errors" - "fmt" "github.com/echovault/echovault/echovault" "github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/constants" "github.com/echovault/echovault/internal/modules/set" "github.com/tidwall/resp" - "net" "slices" "strconv" "strings" - "sync" "testing" ) -var mockServer *echovault.EchoVault -var addr = "localhost" -var port int +func Test_Set(t *testing.T) { + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } -func init() { - port, _ = internal.GetFreePort() - mockServer, _ = echovault.NewEchoVault( + mockServer, err := echovault.NewEchoVault( echovault.WithConfig(config.Config{ - BindAddr: addr, + BindAddr: "localhost", Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) - wg := sync.WaitGroup{} - wg.Add(1) + if err != nil { + t.Error(err) + return + } + go func() { - wg.Done() mockServer.Start() }() - wg.Wait() -} -func Test_HandleSADD(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - preset bool - presetValue interface{} - key string - command []string - expectedValue *set.Set - expectedResponse int - expectedError error - }{ - { - name: "1. Create new set on a non-existent key, return count of added elements", - preset: false, - presetValue: nil, - key: "SaddKey1", - command: []string{"SADD", "SaddKey1", "one", "two", "three", "four"}, - expectedValue: set.NewSet([]string{"one", "two", "three", "four"}), - expectedResponse: 4, - expectedError: nil, - }, - { - name: "2. Add members to an exiting set, skip members that already exist in the set, return added count.", - preset: true, - presetValue: set.NewSet([]string{"one", "two", "three", "four"}), - key: "SaddKey2", - command: []string{"SADD", "SaddKey2", "three", "four", "five", "six", "seven"}, - expectedValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven"}), - expectedResponse: 3, - expectedError: nil, - }, - { - name: "3. Throw error when trying to add to a key that does not hold a set", - preset: true, - presetValue: "Default value", - key: "SaddKey3", - command: []string{"SADD", "SaddKey3", "member"}, - expectedResponse: 0, - expectedError: errors.New("value at key SaddKey3 is not a set"), - }, - { - name: "4. Command too short", - preset: false, - key: "SaddKey4", - command: []string{"SADD", "SaddKey4"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + t.Cleanup(func() { + mockServer.ShutDown() + }) + + t.Run("Test_HandleSADD", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + preset bool + presetValue interface{} + key string + command []string + expectedValue *set.Set + expectedResponse int + expectedError error + }{ + { + name: "1. Create new set on a non-existent key, return count of added elements", + preset: false, + presetValue: nil, + key: "SaddKey1", + command: []string{"SADD", "SaddKey1", "one", "two", "three", "four"}, + expectedValue: set.NewSet([]string{"one", "two", "three", "four"}), + expectedResponse: 4, + expectedError: nil, + }, + { + name: "2. Add members to an exiting set, skip members that already exist in the set, return added count.", + preset: true, + presetValue: set.NewSet([]string{"one", "two", "three", "four"}), + key: "SaddKey2", + command: []string{"SADD", "SaddKey2", "three", "four", "five", "six", "seven"}, + expectedValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven"}), + expectedResponse: 3, + expectedError: nil, + }, + { + name: "3. Throw error when trying to add to a key that does not hold a set", + preset: true, + presetValue: "Default value", + key: "SaddKey3", + command: []string{"SADD", "SaddKey3", "member"}, + expectedResponse: 0, + expectedError: errors.New("value at key SaddKey3 is not a set"), + }, + { + name: "4. Command too short", + preset: false, + key: "SaddKey4", + command: []string{"SADD", "SaddKey4"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = "ok" - case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} - for _, element := range test.presetValue.(*set.Set).GetAll() { - command = append(command, []resp.Value{resp.StringValue(element)}...) + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -141,142 +166,290 @@ func Test_HandleSADD(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + // Check if the resulting set(s) contain the expected members. + if test.expectedValue == nil { + return + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if err := client.WriteArray([]resp.Value{resp.StringValue("SMEMBERS"), resp.StringValue(test.key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.key, test.expectedValue.Cardinality(), len(res.Array())) + } + + for _, item := range res.Array() { + if !test.expectedValue.Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) + } + } + }) + } + }) + + t.Run("Test_HandleSCARD", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValue interface{} + key string + command []string + expectedValue *set.Set + expectedResponse int + expectedError error + }{ + { + name: "1. Get cardinality of valid set.", + presetValue: set.NewSet([]string{"one", "two", "three", "four"}), + key: "ScardKey1", + command: []string{"SCARD", "ScardKey1"}, + expectedValue: nil, + expectedResponse: 4, + expectedError: nil, + }, + { + name: "2. Return 0 when trying to get cardinality on non-existent key", + presetValue: nil, + key: "ScardKey2", + command: []string{"SCARD", "ScardKey2"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "3. Throw error when trying to get cardinality of a value that is not a set", + presetValue: "Default value", + key: "ScardKey3", + command: []string{"SCARD", "ScardKey3"}, + expectedResponse: 0, + expectedError: errors.New("value at key ScardKey3 is not a set"), + }, + { + name: "4. Command too short", + key: "ScardKey4", + command: []string{"SCARD"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Command too long", + key: "ScardKey5", + command: []string{"SCARD", "ScardKey5", "ScardKey5"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } - // Check if the resulting set(s) contain the expected members. - if test.expectedValue == nil { - return - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string - if err := client.WriteArray([]resp.Value{resp.StringValue("SMEMBERS"), resp.StringValue(test.key)}); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } - if len(res.Array()) != test.expectedValue.Cardinality() { - t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", - test.key, test.expectedValue.Cardinality(), len(res.Array())) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - for _, item := range res.Array() { - if !test.expectedValue.Contains(item.String()) { - t.Errorf("unexpected memeber \"%s\", in response", item.String()) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - } - }) - } -} -func Test_HandleSCARD(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValue interface{} - key string - command []string - expectedValue *set.Set - expectedResponse int - expectedError error - }{ - { - name: "1. Get cardinality of valid set.", - presetValue: set.NewSet([]string{"one", "two", "three", "four"}), - key: "ScardKey1", - command: []string{"SCARD", "ScardKey1"}, - expectedValue: nil, - expectedResponse: 4, - expectedError: nil, - }, - { - name: "2. Return 0 when trying to get cardinality on non-existent key", - presetValue: nil, - key: "ScardKey2", - command: []string{"SCARD", "ScardKey2"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "3. Throw error when trying to get cardinality of a value that is not a set", - presetValue: "Default value", - key: "ScardKey3", - command: []string{"SCARD", "ScardKey3"}, - expectedResponse: 0, - expectedError: errors.New("value at key ScardKey3 is not a set"), - }, - { - name: "4. Command too short", - key: "ScardKey4", - command: []string{"SCARD"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Command too long", - key: "ScardKey5", - command: []string{"SCARD", "ScardKey5", "ScardKey5"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } - expected = "ok" - case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} - for _, element := range test.presetValue.(*set.Set).GetAll() { - command = append(command, []resp.Value{resp.StringValue(element)}...) + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleSDIFF", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedResponse []string + expectedError error + }{ + { + name: "1. Get the difference between 2 sets.", + presetValues: map[string]interface{}{ + "SdiffKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), + "SdiffKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), + }, + command: []string{"SDIFF", "SdiffKey1", "SdiffKey2"}, + expectedResponse: []string{"one", "two"}, + expectedError: nil, + }, + { + name: "2. Get the difference between 3 sets.", + presetValues: map[string]interface{}{ + "SdiffKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SdiffKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SdiffKey5": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SDIFF", "SdiffKey3", "SdiffKey4", "SdiffKey5"}, + expectedResponse: []string{"three", "four", "five", "six"}, + expectedError: nil, + }, + { + name: "3. Return base set element if base set is the only valid set", + presetValues: map[string]interface{}{ + "SdiffKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SdiffKey7": "Default value", + "SdiffKey8": "123456789", + }, + command: []string{"SDIFF", "SdiffKey6", "SdiffKey7", "SdiffKey8"}, + expectedResponse: []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}, + expectedError: nil, + }, + { + name: "4. Throw error when base set is not a set.", + presetValues: map[string]interface{}{ + "SdiffKey9": "Default value", + "SdiffKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SdiffKey11": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SDIFF", "SdiffKey9", "SdiffKey10", "SdiffKey11"}, + expectedResponse: nil, + expectedError: errors.New("value at key SdiffKey9 is not a set"), + }, + { + name: "5. Throw error when base set is non-existent.", + presetValues: map[string]interface{}{ + "SdiffKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SdiffKey13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SDIFF", "non-existent", "SdiffKey7", "SdiffKey8"}, + expectedResponse: nil, + expectedError: errors.New("key for base set \"non-existent\" does not exist"), + }, + { + name: "6. Command too short", + command: []string{"SDIFF"}, + expectedResponse: []string{}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -287,133 +460,776 @@ func Test_HandleSCARD(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length \"%d\", got \"%d\"", + len(test.expectedResponse), len(res.Array())) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } + } + }) + } + }) + + t.Run("Test_HandleSDIFFSTORE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + destination string + command []string + expectedValue *set.Set + expectedResponse int + expectedError error + }{ + { + name: "1. Get the difference between 2 sets.", + presetValues: map[string]interface{}{ + "SdiffStoreKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), + "SdiffStoreKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), + }, + destination: "SdiffStoreDestination1", + command: []string{"SDIFFSTORE", "SdiffStoreDestination1", "SdiffStoreKey1", "SdiffStoreKey2"}, + expectedValue: set.NewSet([]string{"one", "two"}), + expectedResponse: 2, + expectedError: nil, + }, + { + name: "2. Get the difference between 3 sets.", + presetValues: map[string]interface{}{ + "SdiffStoreKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SdiffStoreKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SdiffStoreKey5": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + destination: "SdiffStoreDestination2", + command: []string{"SDIFFSTORE", "SdiffStoreDestination2", "SdiffStoreKey3", "SdiffStoreKey4", "SdiffStoreKey5"}, + expectedValue: set.NewSet([]string{"three", "four", "five", "six"}), + expectedResponse: 4, + expectedError: nil, + }, + { + name: "3. Return base set element if base set is the only valid set", + presetValues: map[string]interface{}{ + "SdiffStoreKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SdiffStoreKey7": "Default value", + "SdiffStoreKey8": "123456789", + }, + destination: "SdiffStoreDestination3", + command: []string{"SDIFFSTORE", "SdiffStoreDestination3", "SdiffStoreKey6", "SdiffStoreKey7", "SdiffStoreKey8"}, + expectedValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + expectedResponse: 8, + expectedError: nil, + }, + { + name: "4. Throw error when base set is not a set.", + presetValues: map[string]interface{}{ + "SdiffStoreKey9": "Default value", + "SdiffStoreKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SdiffStoreKey11": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + destination: "SdiffStoreDestination4", + command: []string{"SDIFFSTORE", "SdiffStoreDestination4", "SdiffStoreKey9", "SdiffStoreKey10", "SdiffStoreKey11"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: errors.New("value at key SdiffStoreKey9 is not a set"), + }, + { + name: "5. Throw error when base set is non-existent.", + destination: "SdiffStoreDestination5", + presetValues: map[string]interface{}{ + "SdiffStoreKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SdiffStoreKey13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SDIFFSTORE", "SdiffStoreDestination5", "non-existent", "SdiffStoreKey7", "SdiffStoreKey8"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: errors.New("key for base set \"non-existent\" does not exist"), + }, + { + name: "6. Command too short", + command: []string{"SDIFFSTORE", "SdiffStoreDestination6"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - }) - } -} + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } -func Test_HandleSDIFF(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedResponse []string - expectedError error - }{ - { - name: "1. Get the difference between 2 sets.", - presetValues: map[string]interface{}{ - "SdiffKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), - "SdiffKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), - }, - command: []string{"SDIFF", "SdiffKey1", "SdiffKey2"}, - expectedResponse: []string{"one", "two"}, - expectedError: nil, - }, - { - name: "2. Get the difference between 3 sets.", - presetValues: map[string]interface{}{ - "SdiffKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SdiffKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SdiffKey5": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SDIFF", "SdiffKey3", "SdiffKey4", "SdiffKey5"}, - expectedResponse: []string{"three", "four", "five", "six"}, - expectedError: nil, - }, - { - name: "3. Return base set element if base set is the only valid set", - presetValues: map[string]interface{}{ - "SdiffKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SdiffKey7": "Default value", - "SdiffKey8": "123456789", - }, - command: []string{"SDIFF", "SdiffKey6", "SdiffKey7", "SdiffKey8"}, - expectedResponse: []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}, - expectedError: nil, - }, - { - name: "4. Throw error when base set is not a set.", - presetValues: map[string]interface{}{ - "SdiffKey9": "Default value", - "SdiffKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SdiffKey11": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SDIFF", "SdiffKey9", "SdiffKey10", "SdiffKey11"}, - expectedResponse: nil, - expectedError: errors.New("value at key SdiffKey9 is not a set"), - }, - { - name: "5. Throw error when base set is non-existent.", - presetValues: map[string]interface{}{ - "SdiffKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SdiffKey13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SDIFF", "non-existent", "SdiffKey7", "SdiffKey8"}, - expectedResponse: nil, - expectedError: errors.New("key for base set \"non-existent\" does not exist"), - }, - { - name: "6. Command too short", - command: []string{"SDIFF"}, - expectedResponse: []string{}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + + // Check if the resulting set(s) contain the expected members. + if test.expectedValue == nil { + return + } + + if err := client.WriteArray([]resp.Value{ + resp.StringValue("SMEMBERS"), + resp.StringValue(test.destination), + }); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.destination, test.expectedValue.Cardinality(), len(res.Array())) + } + + for _, item := range res.Array() { + if !test.expectedValue.Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) + } + } + }) + } + }) + + t.Run("Test_HandleSINTER", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedResponse []string + expectedError error + }{ + { + name: "1. Get the intersection between 2 sets.", + presetValues: map[string]interface{}{ + "SinterKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), + "SinterKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), + }, + command: []string{"SINTER", "SinterKey1", "SinterKey2"}, + expectedResponse: []string{"three", "four", "five"}, + expectedError: nil, + }, + { + name: "2. Get the intersection between 3 sets.", + presetValues: map[string]interface{}{ + "SinterKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SinterKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), + "SinterKey5": set.NewSet([]string{"one", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SINTER", "SinterKey3", "SinterKey4", "SinterKey5"}, + expectedResponse: []string{"one", "eight"}, + expectedError: nil, + }, + { + name: "3. Throw an error if any of the provided keys are not sets", + presetValues: map[string]interface{}{ + "SinterKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SinterKey7": "Default value", + "SinterKey8": set.NewSet([]string{"one"}), + }, + command: []string{"SINTER", "SinterKey6", "SinterKey7", "SinterKey8"}, + expectedResponse: nil, + expectedError: errors.New("value at key SinterKey7 is not a set"), + }, + { + name: "4. Throw error when base set is not a set.", + presetValues: map[string]interface{}{ + "SinterKey9": "Default value", + "SinterKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SinterKey11": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SINTER", "SinterKey9", "SinterKey10", "SinterKey11"}, + expectedResponse: nil, + expectedError: errors.New("value at key SinterKey9 is not a set"), + }, + { + name: "5. If any of the keys does not exist, return an empty array.", + presetValues: map[string]interface{}{ + "SinterKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SinterKey13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SINTER", "non-existent", "SinterKey7", "SinterKey8"}, + expectedResponse: []string{}, + expectedError: nil, + }, + { + name: "6. Command too short", + command: []string{"SINTER"}, + expectedResponse: []string{}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length \"%d\", got \"%d\"", + len(test.expectedResponse), len(res.Array())) + } + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } + } + }) + } + }) + + t.Run("Test_HandleSINTERCARD", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedResponse int + expectedError error + }{ + { + name: "1. Get the full intersect cardinality between 2 sets.", + presetValues: map[string]interface{}{ + "SinterCardKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), + "SinterCardKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), + }, + command: []string{"SINTERCARD", "SinterCardKey1", "SinterCardKey2"}, + expectedResponse: 3, + expectedError: nil, + }, + { + name: "2. Get an intersect cardinality between 2 sets with a limit", + presetValues: map[string]interface{}{ + "SinterCardKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"}), + "SinterCardKey4": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"}), + }, + command: []string{"SINTERCARD", "SinterCardKey3", "SinterCardKey4", "LIMIT", "3"}, + expectedResponse: 3, + expectedError: nil, + }, + { + name: "3. Get the full intersect cardinality between 3 sets.", + presetValues: map[string]interface{}{ + "SinterCardKey5": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SinterCardKey6": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), + "SinterCardKey7": set.NewSet([]string{"one", "seven", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SINTERCARD", "SinterCardKey5", "SinterCardKey6", "SinterCardKey7"}, + expectedResponse: 2, + expectedError: nil, + }, + { + name: "4. Get the intersection of 3 sets with a limit", + presetValues: map[string]interface{}{ + "SinterCardKey8": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SinterCardKey9": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), + "SinterCardKey10": set.NewSet([]string{"one", "two", "seven", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SINTERCARD", "SinterCardKey8", "SinterCardKey9", "SinterCardKey10", "LIMIT", "2"}, + expectedResponse: 2, + expectedError: nil, + }, + { + name: "5. Return 0 if any of the keys does not exist", + presetValues: map[string]interface{}{ + "SinterCardKey11": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SinterCardKey12": "Default value", + "SinterCardKey13": set.NewSet([]string{"one"}), + }, + command: []string{"SINTERCARD", "SinterCardKey11", "SinterCardKey12", "SinterCardKey13", "non-existent"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "6. Throw error when one of the keys is not a valid set.", + presetValues: map[string]interface{}{ + "SinterCardKey14": "Default value", + "SinterCardKey15": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SinterCardKey16": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SINTERCARD", "SinterCardKey14", "SinterCardKey15", "SinterCardKey16"}, + expectedResponse: 0, + expectedError: errors.New("value at key SinterCardKey14 is not a set"), + }, + { + name: "7. Command too short", + command: []string{"SINTERCARD"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response array of length \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + }) + } + + }) + + t.Run("Test_HandleSINTERSTORE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + destination string + command []string + expectedValue *set.Set + expectedResponse int + expectedError error + }{ + { + name: "1. Get the intersection between 2 sets and store it at the destination.", + presetValues: map[string]interface{}{ + "SinterStoreKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), + "SinterStoreKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), + }, + destination: "SinterStoreDestination1", + command: []string{"SINTERSTORE", "SinterStoreDestination1", "SinterStoreKey1", "SinterStoreKey2"}, + expectedValue: set.NewSet([]string{"three", "four", "five"}), + expectedResponse: 3, + expectedError: nil, + }, + { + name: "2. Get the intersection between 3 sets and store it at the destination key.", + presetValues: map[string]interface{}{ + "SinterStoreKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SinterStoreKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), + "SinterStoreKey5": set.NewSet([]string{"one", "seven", "eight", "nine", "ten", "twelve"}), + }, + destination: "SinterStoreDestination2", + command: []string{"SINTERSTORE", "SinterStoreDestination2", "SinterStoreKey3", "SinterStoreKey4", "SinterStoreKey5"}, + expectedValue: set.NewSet([]string{"one", "eight"}), + expectedResponse: 2, + expectedError: nil, + }, + { + name: "3. Throw error when any of the keys is not a set", + presetValues: map[string]interface{}{ + "SinterStoreKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SinterStoreKey7": "Default value", + "SinterStoreKey8": set.NewSet([]string{"one"}), + }, + destination: "SinterStoreDestination3", + command: []string{"SINTERSTORE", "SinterStoreDestination3", "SinterStoreKey6", "SinterStoreKey7", "SinterStoreKey8"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: errors.New("value at key SinterStoreKey7 is not a set"), + }, + { + name: "4. Throw error when base set is not a set.", + presetValues: map[string]interface{}{ + "SinterStoreKey9": "Default value", + "SinterStoreKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SinterStoreKey11": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + destination: "SinterStoreDestination4", + command: []string{"SINTERSTORE", "SinterStoreDestination4", "SinterStoreKey9", "SinterStoreKey10", "SinterStoreKey11"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: errors.New("value at key SinterStoreKey9 is not a set"), + }, + { + name: "5. Return an empty intersection if one of the keys does not exist.", + destination: "SinterStoreDestination5", + presetValues: map[string]interface{}{ + "SinterStoreKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SinterStoreKey13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SINTERSTORE", "SinterStoreDestination5", "non-existent", "SinterStoreKey7", "SinterStoreKey8"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "6. Command too short", + command: []string{"SINTERSTORE", "SinterStoreDestination6"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + + // Check if the resulting set(s) contain the expected members. + if test.expectedValue == nil { + return + } + + if err := client.WriteArray([]resp.Value{ + resp.StringValue("SMEMBERS"), + resp.StringValue(test.destination), + }); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.destination, test.expectedValue.Cardinality(), len(res.Array())) + } + + for _, item := range res.Array() { + if !test.expectedValue.Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) + } + } + }) + } + }) + + t.Run("Test_HandleSISMEMBER", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValue interface{} + key string + command []string + expectedResponse int + expectedError error + }{ + { + name: "1. Return 1 when element is a member of the set", + presetValue: set.NewSet([]string{"one", "two", "three", "four"}), + key: "SIsMemberKey1", + command: []string{"SISMEMBER", "SIsMemberKey1", "three"}, + expectedResponse: 1, + expectedError: nil, + }, + { + name: "2. Return 0 when element is not a member of the set", + presetValue: set.NewSet([]string{"one", "two", "three", "four"}), + key: "SIsMemberKey2", + command: []string{"SISMEMBER", "SIsMemberKey2", "five"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "3. Throw error when trying to assert membership when the key does not hold a valid set", + presetValue: "Default value", + key: "SIsMemberKey3", + command: []string{"SISMEMBER", "SIsMemberKey3", "one"}, + expectedResponse: 0, + expectedError: errors.New("value at key SIsMemberKey3 is not a set"), + }, + { + name: "4. Command too short", + key: "SIsMemberKey4", + command: []string{"SISMEMBER", "SIsMemberKey4"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Command too long", + key: "SIsMemberKey5", + command: []string{"SISMEMBER", "SIsMemberKey5", "one", "two", "three"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { case string: command = []resp.Value{ resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), } expected = "ok" case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} - for _, element := range value.(*set.Set).GetAll() { + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { command = append(command, []resp.Value{resp.StringValue(element)}...) } - expected = strconv.Itoa(value.(*set.Set).Cardinality()) + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) } if err = client.WriteArray(command); err != nil { @@ -428,149 +1244,112 @@ func Test_HandleSDIFF(t *testing.T) { t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - return - } - - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length \"%d\", got \"%d\"", - len(test.expectedResponse), len(res.Array())) - } - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.String()) { - t.Errorf("unexpected element \"%s\" in response", item.String()) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - } - }) - } -} -func Test_HandleSDIFFSTORE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - destination string - command []string - expectedValue *set.Set - expectedResponse int - expectedError error - }{ - { - name: "1. Get the difference between 2 sets.", - presetValues: map[string]interface{}{ - "SdiffStoreKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), - "SdiffStoreKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), - }, - destination: "SdiffStoreDestination1", - command: []string{"SDIFFSTORE", "SdiffStoreDestination1", "SdiffStoreKey1", "SdiffStoreKey2"}, - expectedValue: set.NewSet([]string{"one", "two"}), - expectedResponse: 2, - expectedError: nil, - }, - { - name: "2. Get the difference between 3 sets.", - presetValues: map[string]interface{}{ - "SdiffStoreKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SdiffStoreKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SdiffStoreKey5": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - destination: "SdiffStoreDestination2", - command: []string{"SDIFFSTORE", "SdiffStoreDestination2", "SdiffStoreKey3", "SdiffStoreKey4", "SdiffStoreKey5"}, - expectedValue: set.NewSet([]string{"three", "four", "five", "six"}), - expectedResponse: 4, - expectedError: nil, - }, - { - name: "3. Return base set element if base set is the only valid set", - presetValues: map[string]interface{}{ - "SdiffStoreKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SdiffStoreKey7": "Default value", - "SdiffStoreKey8": "123456789", - }, - destination: "SdiffStoreDestination3", - command: []string{"SDIFFSTORE", "SdiffStoreDestination3", "SdiffStoreKey6", "SdiffStoreKey7", "SdiffStoreKey8"}, - expectedValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - expectedResponse: 8, - expectedError: nil, - }, - { - name: "4. Throw error when base set is not a set.", - presetValues: map[string]interface{}{ - "SdiffStoreKey9": "Default value", - "SdiffStoreKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SdiffStoreKey11": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - destination: "SdiffStoreDestination4", - command: []string{"SDIFFSTORE", "SdiffStoreDestination4", "SdiffStoreKey9", "SdiffStoreKey10", "SdiffStoreKey11"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: errors.New("value at key SdiffStoreKey9 is not a set"), - }, - { - name: "5. Throw error when base set is non-existent.", - destination: "SdiffStoreDestination5", - presetValues: map[string]interface{}{ - "SdiffStoreKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SdiffStoreKey13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SDIFFSTORE", "SdiffStoreDestination5", "non-existent", "SdiffStoreKey7", "SdiffStoreKey8"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: errors.New("key for base set \"non-existent\" does not exist"), - }, - { - name: "6. Command too short", - command: []string{"SDIFFSTORE", "SdiffStoreDestination6"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleSMEMBERS", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedResponse []string + expectedError error + }{ + { + name: "1. Return all the members of the set.", + key: "SmembersKey1", + presetValue: set.NewSet([]string{"one", "two", "three", "four", "five"}), + command: []string{"SMEMBERS", "SmembersKey1"}, + expectedResponse: []string{"one", "two", "three", "four", "five"}, + expectedError: nil, + }, + { + name: "2. If the key does not exist, return an empty array.", + key: "SmembersKey2", + presetValue: nil, + command: []string{"SMEMBERS", "SmembersKey2"}, + expectedResponse: nil, + expectedError: nil, + }, + { + name: "3. Throw error when the provided key is not a set.", + key: "SmembersKey3", + presetValue: "Default value", + command: []string{"SMEMBERS", "SmembersKey3"}, + expectedResponse: nil, + expectedError: errors.New("value at key SmembersKey3 is not a set"), + }, + { + name: "4. Command too short", + command: []string{"SMEMBERS"}, + expectedResponse: []string{}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Command too long", + command: []string{"SMEMBERS", "SmembersKey5", "SmembersKey6"}, + expectedResponse: []string{}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { case string: command = []resp.Value{ resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), } expected = "ok" case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} - for _, element := range value.(*set.Set).GetAll() { + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { command = append(command, []resp.Value{resp.StringValue(element)}...) } - expected = strconv.Itoa(value.(*set.Set).Cardinality()) + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) } if err = client.WriteArray(command); err != nil { @@ -585,157 +1364,119 @@ func Test_HandleSDIFFSTORE(t *testing.T) { t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - - // Check if the resulting set(s) contain the expected members. - if test.expectedValue == nil { - return - } - - if err := client.WriteArray([]resp.Value{ - resp.StringValue("SMEMBERS"), - resp.StringValue(test.destination), - }); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != test.expectedValue.Cardinality() { - t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", - test.destination, test.expectedValue.Cardinality(), len(res.Array())) - } - - for _, item := range res.Array() { - if !test.expectedValue.Contains(item.String()) { - t.Errorf("unexpected memeber \"%s\", in response", item.String()) - } - } - }) - } -} -func Test_HandleSINTER(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedResponse []string - expectedError error - }{ - { - name: "1. Get the intersection between 2 sets.", - presetValues: map[string]interface{}{ - "SinterKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), - "SinterKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), - }, - command: []string{"SINTER", "SinterKey1", "SinterKey2"}, - expectedResponse: []string{"three", "four", "five"}, - expectedError: nil, - }, - { - name: "2. Get the intersection between 3 sets.", - presetValues: map[string]interface{}{ - "SinterKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SinterKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), - "SinterKey5": set.NewSet([]string{"one", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SINTER", "SinterKey3", "SinterKey4", "SinterKey5"}, - expectedResponse: []string{"one", "eight"}, - expectedError: nil, - }, - { - name: "3. Throw an error if any of the provided keys are not sets", - presetValues: map[string]interface{}{ - "SinterKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SinterKey7": "Default value", - "SinterKey8": set.NewSet([]string{"one"}), - }, - command: []string{"SINTER", "SinterKey6", "SinterKey7", "SinterKey8"}, - expectedResponse: nil, - expectedError: errors.New("value at key SinterKey7 is not a set"), - }, - { - name: "4. Throw error when base set is not a set.", - presetValues: map[string]interface{}{ - "SinterKey9": "Default value", - "SinterKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SinterKey11": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SINTER", "SinterKey9", "SinterKey10", "SinterKey11"}, - expectedResponse: nil, - expectedError: errors.New("value at key SinterKey9 is not a set"), - }, - { - name: "5. If any of the keys does not exist, return an empty array.", - presetValues: map[string]interface{}{ - "SinterKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SinterKey13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SINTER", "non-existent", "SinterKey7", "SinterKey8"}, - expectedResponse: []string{}, - expectedError: nil, - }, - { - name: "6. Command too short", - command: []string{"SINTER"}, - expectedResponse: []string{}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length \"%d\", got \"%d\"", + len(test.expectedResponse), len(res.Array())) + } + + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } + } + }) + } + }) + + t.Run("Test_HandleSMISMEMBER", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValue interface{} + key string + command []string + expectedResponse []int + expectedError error + }{ + { + // 1. Return set membership status for multiple elements + // Return 1 for present and 0 for absent + // The placement of the membership status flag should me consistent with the order the elements + // are in within the original command + name: "1. Return set membership status for multiple elements", + presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven"}), + key: "SmismemberKey1", + command: []string{"SMISMEMBER", "SmismemberKey1", "three", "four", "five", "six", "eight", "nine", "seven"}, + expectedResponse: []int{1, 1, 1, 1, 0, 0, 1}, + expectedError: nil, + }, + { + name: "2. If the set key does not exist, return an array of zeroes as long as the list of members", + presetValue: nil, + key: "SmismemberKey2", + command: []string{"SMISMEMBER", "SmismemberKey2", "one", "two", "three", "four"}, + expectedResponse: []int{0, 0, 0, 0}, + expectedError: nil, + }, + { + name: "3. Throw error when trying to assert membership when the key does not hold a valid set", + presetValue: "Default value", + key: "SmismemberKey3", + command: []string{"SMISMEMBER", "SmismemberKey3", "one"}, + expectedResponse: nil, + expectedError: errors.New("value at key SmismemberKey3 is not a set"), + }, + { + name: "4. Command too short", + presetValue: nil, + key: "SmismemberKey4", + command: []string{"SMISMEMBER", "SmismemberKey4"}, + expectedResponse: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { case string: command = []resp.Value{ resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), } expected = "ok" case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} - for _, element := range value.(*set.Set).GetAll() { + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { command = append(command, []resp.Value{resp.StringValue(element)}...) } - expected = strconv.Itoa(value.(*set.Set).Cardinality()) + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) } if err = client.WriteArray(command); err != nil { @@ -750,148 +1491,320 @@ func Test_HandleSINTER(t *testing.T) { t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - return - } - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length \"%d\", got \"%d\"", - len(test.expectedResponse), len(res.Array())) - } + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length \"%d\", got \"%d\"", + len(test.expectedResponse), len(res.Array())) + } - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.String()) { - t.Errorf("unexpected element \"%s\" in response", item.String()) + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.Integer()) { + t.Errorf("unexpected element \"%d\" in response", item.Integer()) + } } - } - }) - } -} + }) + } + }) + + t.Run("Test_HandleSMOVE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedValues map[string]interface{} + expectedResponse int + expectedError error + }{ + { + name: "1. Return 1 after a successful move of a member from source set to destination set", + presetValues: map[string]interface{}{ + "SmoveSource1": set.NewSet([]string{"one", "two", "three", "four"}), + "SmoveDestination1": set.NewSet([]string{"five", "six", "seven", "eight"}), + }, + command: []string{"SMOVE", "SmoveSource1", "SmoveDestination1", "four"}, + expectedValues: map[string]interface{}{ + "SmoveSource1": set.NewSet([]string{"one", "two", "three"}), + "SmoveDestination1": set.NewSet([]string{"four", "five", "six", "seven", "eight"}), + }, + expectedResponse: 1, + expectedError: nil, + }, + { + name: "2. Return 0 when trying to move a member from source set to destination set when it doesn't exist in source", + presetValues: map[string]interface{}{ + "SmoveSource2": set.NewSet([]string{"one", "two", "three", "four", "five"}), + "SmoveDestination2": set.NewSet([]string{"five", "six", "seven", "eight"}), + }, + command: []string{"SMOVE", "SmoveSource2", "SmoveDestination2", "six"}, + expectedValues: map[string]interface{}{ + "SmoveSource2": set.NewSet([]string{"one", "two", "three", "four", "five"}), + "SmoveDestination2": set.NewSet([]string{"five", "six", "seven", "eight"}), + }, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "3. Return error when the source key is not a set", + presetValues: map[string]interface{}{ + "SmoveSource3": "Default value", + "SmoveDestination3": set.NewSet([]string{"five", "six", "seven", "eight"}), + }, + command: []string{"SMOVE", "SmoveSource3", "SmoveDestination3", "five"}, + expectedValues: map[string]interface{}{ + "SmoveSource3": "Default value", + "SmoveDestination3": set.NewSet([]string{"five", "six", "seven", "eight"}), + }, + expectedResponse: 0, + expectedError: errors.New("source is not a set"), + }, + { + name: "4. Return error when the destination key is not a set", + presetValues: map[string]interface{}{ + "SmoveSource4": set.NewSet([]string{"one", "two", "three", "four", "five"}), + "SmoveDestination4": "Default value", + }, + command: []string{"SMOVE", "SmoveSource4", "SmoveDestination4", "five"}, + expectedValues: map[string]interface{}{ + "SmoveSource4": set.NewSet([]string{"one", "two", "three", "four", "five"}), + "SmoveDestination4": "Default value", + }, + expectedResponse: 0, + expectedError: errors.New("destination is not a set"), + }, + { + name: "5. Command too short", + presetValues: nil, + command: []string{"SMOVE", "SmoveSource5", "SmoveSource6"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + presetValues: nil, + command: []string{"SMOVE", "SmoveSource5", "SmoveSource6", "member1", "member2"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + + // Check if the resulting set(s) contain the expected members. + if test.expectedValues == nil { + return + } + + for key, value := range test.expectedValues { + switch value.(type) { + case string: + if err := client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + if res.String() != value.(string) { + t.Errorf("expected value at key \"%s\" to be \"%s\", got \"%s\"", key, value.(string), res.String()) + } + case *set.Set: + if err := client.WriteArray([]resp.Value{ + resp.StringValue("SMEMBERS"), + resp.StringValue(key), + }); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != value.(*set.Set).Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + key, value.(*set.Set).Cardinality(), len(res.Array())) + } + + for _, item := range res.Array() { + if !value.(*set.Set).Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) + } + } + } + } + }) + } + }) + + t.Run("Test_HandleSPOP", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedValue int // The final cardinality of the resulting set + expectedResponse []string + expectedError error + }{ + { + name: "1. Return multiple popped elements and modify the set", + key: "SpopKey1", + presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + command: []string{"SPOP", "SpopKey1", "3"}, + expectedValue: 5, + expectedResponse: []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}, + expectedError: nil, + }, + { + name: "2. Return error when the source key is not a set", + key: "SpopKey2", + presetValue: "Default value", + command: []string{"SPOP", "SpopKey2"}, + expectedValue: 0, + expectedResponse: nil, + expectedError: errors.New("value at SpopKey2 is not a set"), + }, + { + name: "3. Command too short", + presetValue: nil, + command: []string{"SPOP"}, + expectedValue: 0, + expectedResponse: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "4. Command too long", + presetValue: nil, + command: []string{"SPOP", "SpopSource5", "SpopSource6", "member1", "member2"}, + expectedValue: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Throw error when count is not an integer", + presetValue: nil, + command: []string{"SPOP", "SpopKey1", "count"}, + expectedValue: 0, + expectedError: errors.New("count must be an integer"), + }, + } -func Test_HandleSINTERCARD(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedResponse int - expectedError error - }{ - { - name: "1. Get the full intersect cardinality between 2 sets.", - presetValues: map[string]interface{}{ - "SinterCardKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), - "SinterCardKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), - }, - command: []string{"SINTERCARD", "SinterCardKey1", "SinterCardKey2"}, - expectedResponse: 3, - expectedError: nil, - }, - { - name: "2. Get an intersect cardinality between 2 sets with a limit", - presetValues: map[string]interface{}{ - "SinterCardKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"}), - "SinterCardKey4": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"}), - }, - command: []string{"SINTERCARD", "SinterCardKey3", "SinterCardKey4", "LIMIT", "3"}, - expectedResponse: 3, - expectedError: nil, - }, - { - name: "3. Get the full intersect cardinality between 3 sets.", - presetValues: map[string]interface{}{ - "SinterCardKey5": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SinterCardKey6": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), - "SinterCardKey7": set.NewSet([]string{"one", "seven", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SINTERCARD", "SinterCardKey5", "SinterCardKey6", "SinterCardKey7"}, - expectedResponse: 2, - expectedError: nil, - }, - { - name: "4. Get the intersection of 3 sets with a limit", - presetValues: map[string]interface{}{ - "SinterCardKey8": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SinterCardKey9": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), - "SinterCardKey10": set.NewSet([]string{"one", "two", "seven", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SINTERCARD", "SinterCardKey8", "SinterCardKey9", "SinterCardKey10", "LIMIT", "2"}, - expectedResponse: 2, - expectedError: nil, - }, - { - name: "5. Return 0 if any of the keys does not exist", - presetValues: map[string]interface{}{ - "SinterCardKey11": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SinterCardKey12": "Default value", - "SinterCardKey13": set.NewSet([]string{"one"}), - }, - command: []string{"SINTERCARD", "SinterCardKey11", "SinterCardKey12", "SinterCardKey13", "non-existent"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "6. Throw error when one of the keys is not a valid set.", - presetValues: map[string]interface{}{ - "SinterCardKey14": "Default value", - "SinterCardKey15": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SinterCardKey16": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SINTERCARD", "SinterCardKey14", "SinterCardKey15", "SinterCardKey16"}, - expectedResponse: 0, - expectedError: errors.New("value at key SinterCardKey14 is not a set"), - }, - { - name: "7. Command too short", - command: []string{"SINTERCARD"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { + switch test.presetValue.(type) { case string: command = []resp.Value{ resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), } expected = "ok" case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} - for _, element := range value.(*set.Set).GetAll() { + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { command = append(command, []resp.Value{resp.StringValue(element)}...) } - expected = strconv.Itoa(value.(*set.Set).Cardinality()) + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) } if err = client.WriteArray(command); err != nil { @@ -906,143 +1819,143 @@ func Test_HandleSINTERCARD(t *testing.T) { t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response array of length \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - }) - } + // Check that each returned element is in the list of expected elements. + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } + } -} + // Check if the resulting set's cardinality is as expected. + if err := client.WriteArray([]resp.Value{resp.StringValue("SCARD"), resp.StringValue(test.key)}); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } -func Test_HandleSINTERSTORE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - destination string - command []string - expectedValue *set.Set - expectedResponse int - expectedError error - }{ - { - name: "1. Get the intersection between 2 sets and store it at the destination.", - presetValues: map[string]interface{}{ - "SinterStoreKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), - "SinterStoreKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), - }, - destination: "SinterStoreDestination1", - command: []string{"SINTERSTORE", "SinterStoreDestination1", "SinterStoreKey1", "SinterStoreKey2"}, - expectedValue: set.NewSet([]string{"three", "four", "five"}), - expectedResponse: 3, - expectedError: nil, - }, - { - name: "2. Get the intersection between 3 sets and store it at the destination key.", - presetValues: map[string]interface{}{ - "SinterStoreKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SinterStoreKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), - "SinterStoreKey5": set.NewSet([]string{"one", "seven", "eight", "nine", "ten", "twelve"}), - }, - destination: "SinterStoreDestination2", - command: []string{"SINTERSTORE", "SinterStoreDestination2", "SinterStoreKey3", "SinterStoreKey4", "SinterStoreKey5"}, - expectedValue: set.NewSet([]string{"one", "eight"}), - expectedResponse: 2, - expectedError: nil, - }, - { - name: "3. Throw error when any of the keys is not a set", - presetValues: map[string]interface{}{ - "SinterStoreKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SinterStoreKey7": "Default value", - "SinterStoreKey8": set.NewSet([]string{"one"}), - }, - destination: "SinterStoreDestination3", - command: []string{"SINTERSTORE", "SinterStoreDestination3", "SinterStoreKey6", "SinterStoreKey7", "SinterStoreKey8"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: errors.New("value at key SinterStoreKey7 is not a set"), - }, - { - name: "4. Throw error when base set is not a set.", - presetValues: map[string]interface{}{ - "SinterStoreKey9": "Default value", - "SinterStoreKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SinterStoreKey11": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - destination: "SinterStoreDestination4", - command: []string{"SINTERSTORE", "SinterStoreDestination4", "SinterStoreKey9", "SinterStoreKey10", "SinterStoreKey11"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: errors.New("value at key SinterStoreKey9 is not a set"), - }, - { - name: "5. Return an empty intersection if one of the keys does not exist.", - destination: "SinterStoreDestination5", - presetValues: map[string]interface{}{ - "SinterStoreKey12": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SinterStoreKey13": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SINTERSTORE", "SinterStoreDestination5", "non-existent", "SinterStoreKey7", "SinterStoreKey8"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "6. Command too short", - command: []string{"SINTERSTORE", "SinterStoreDestination6"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if res.Integer() != test.expectedValue { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.key, test.expectedValue, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleSRANDMEMBER", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedValue int // The final cardinality of the resulting set + allowRepeat bool + expectedResponse []string + expectedError error + }{ + { + // 1. Return multiple random elements without removing them + // Count is positive, do not allow repeated elements + name: "1. Return multiple random elements without removing them", + key: "SRandMemberKey1", + presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + command: []string{"SRANDMEMBER", "SRandMemberKey1", "3"}, + expectedValue: 8, + allowRepeat: false, + expectedResponse: []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}, + expectedError: nil, + }, + { + // 2. Return multiple random elements without removing them + // Count is negative, so allow repeated numbers + name: "2. Return multiple random elements without removing them", + key: "SRandMemberKey2", + presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + command: []string{"SRANDMEMBER", "SRandMemberKey2", "-5"}, + expectedValue: 8, + allowRepeat: true, + expectedResponse: []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}, + expectedError: nil, + }, + { + name: "3. Return error when the source key is not a set", + key: "SRandMemberKey3", + presetValue: "Default value", + command: []string{"SRANDMEMBER", "SRandMemberKey3"}, + expectedValue: 0, + expectedResponse: []string{}, + expectedError: errors.New("value at SRandMemberKey3 is not a set"), + }, + { + name: "4. Command too short", + command: []string{"SRANDMEMBER"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Command too long", + command: []string{"SRANDMEMBER", "SRandMemberSource5", "SRandMemberSource6", "member1", "member2"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Throw error when count is not an integer", + command: []string{"SRANDMEMBER", "SRandMemberKey1", "count"}, + expectedError: errors.New("count must be an integer"), + }, + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { case string: command = []resp.Value{ resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), } expected = "ok" case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} - for _, element := range value.(*set.Set).GetAll() { + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { command = append(command, []resp.Value{resp.StringValue(element)}...) } - expected = strconv.Itoa(value.(*set.Set).Cardinality()) + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) } if err = client.WriteArray(command); err != nil { @@ -1057,137 +1970,10 @@ func Test_HandleSINTERSTORE(t *testing.T) { t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - - // Check if the resulting set(s) contain the expected members. - if test.expectedValue == nil { - return - } - - if err := client.WriteArray([]resp.Value{ - resp.StringValue("SMEMBERS"), - resp.StringValue(test.destination), - }); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != test.expectedValue.Cardinality() { - t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", - test.destination, test.expectedValue.Cardinality(), len(res.Array())) - } - - for _, item := range res.Array() { - if !test.expectedValue.Contains(item.String()) { - t.Errorf("unexpected memeber \"%s\", in response", item.String()) - } - } - }) - } -} -func Test_HandleSISMEMBER(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValue interface{} - key string - command []string - expectedResponse int - expectedError error - }{ - { - name: "1. Return 1 when element is a member of the set", - presetValue: set.NewSet([]string{"one", "two", "three", "four"}), - key: "SIsMemberKey1", - command: []string{"SISMEMBER", "SIsMemberKey1", "three"}, - expectedResponse: 1, - expectedError: nil, - }, - { - name: "2. Return 0 when element is not a member of the set", - presetValue: set.NewSet([]string{"one", "two", "three", "four"}), - key: "SIsMemberKey2", - command: []string{"SISMEMBER", "SIsMemberKey2", "five"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "3. Throw error when trying to assert membership when the key does not hold a valid set", - presetValue: "Default value", - key: "SIsMemberKey3", - command: []string{"SISMEMBER", "SIsMemberKey3", "one"}, - expectedResponse: 0, - expectedError: errors.New("value at key SIsMemberKey3 is not a set"), - }, - { - name: "4. Command too short", - key: "SIsMemberKey4", - command: []string{"SISMEMBER", "SIsMemberKey4"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Command too long", - key: "SIsMemberKey5", - command: []string{"SISMEMBER", "SIsMemberKey5", "one", "two", "three"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), - } - expected = "ok" - case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} - for _, element := range test.presetValue.(*set.Set).GetAll() { - command = append(command, []resp.Value{resp.StringValue(element)}...) - } - expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -1198,387 +1984,126 @@ func Test_HandleSISMEMBER(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - }) - } -} - -func Test_HandleSMEMBERS(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedResponse []string - expectedError error - }{ - { - name: "1. Return all the members of the set.", - key: "SmembersKey1", - presetValue: set.NewSet([]string{"one", "two", "three", "four", "five"}), - command: []string{"SMEMBERS", "SmembersKey1"}, - expectedResponse: []string{"one", "two", "three", "four", "five"}, - expectedError: nil, - }, - { - name: "2. If the key does not exist, return an empty array.", - key: "SmembersKey2", - presetValue: nil, - command: []string{"SMEMBERS", "SmembersKey2"}, - expectedResponse: nil, - expectedError: nil, - }, - { - name: "3. Throw error when the provided key is not a set.", - key: "SmembersKey3", - presetValue: "Default value", - command: []string{"SMEMBERS", "SmembersKey3"}, - expectedResponse: nil, - expectedError: errors.New("value at key SmembersKey3 is not a set"), - }, - { - name: "4. Command too short", - command: []string{"SMEMBERS"}, - expectedResponse: []string{}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Command too long", - command: []string{"SMEMBERS", "SmembersKey5", "SmembersKey6"}, - expectedResponse: []string{}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), - } - expected = "ok" - case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} - for _, element := range test.presetValue.(*set.Set).GetAll() { - command = append(command, []resp.Value{resp.StringValue(element)}...) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } - expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length \"%d\", got \"%d\"", - len(test.expectedResponse), len(res.Array())) - } - - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.String()) { - t.Errorf("unexpected element \"%s\" in response", item.String()) + // Check that each returned element is in the list of expected elements. + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } } - } - }) - } -} -func Test_HandleSMISMEMBER(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValue interface{} - key string - command []string - expectedResponse []int - expectedError error - }{ - { - // 1. Return set membership status for multiple elements - // Return 1 for present and 0 for absent - // The placement of the membership status flag should me consistent with the order the elements - // are in within the original command - name: "1. Return set membership status for multiple elements", - presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven"}), - key: "SmismemberKey1", - command: []string{"SMISMEMBER", "SmismemberKey1", "three", "four", "five", "six", "eight", "nine", "seven"}, - expectedResponse: []int{1, 1, 1, 1, 0, 0, 1}, - expectedError: nil, - }, - { - name: "2. If the set key does not exist, return an array of zeroes as long as the list of members", - presetValue: nil, - key: "SmismemberKey2", - command: []string{"SMISMEMBER", "SmismemberKey2", "one", "two", "three", "four"}, - expectedResponse: []int{0, 0, 0, 0}, - expectedError: nil, - }, - { - name: "3. Throw error when trying to assert membership when the key does not hold a valid set", - presetValue: "Default value", - key: "SmismemberKey3", - command: []string{"SMISMEMBER", "SmismemberKey3", "one"}, - expectedResponse: nil, - expectedError: errors.New("value at key SmismemberKey3 is not a set"), - }, - { - name: "4. Command too short", - presetValue: nil, - key: "SmismemberKey4", - command: []string{"SMISMEMBER", "SmismemberKey4"}, - expectedResponse: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), - } - expected = "ok" - case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} - for _, element := range test.presetValue.(*set.Set).GetAll() { - command = append(command, []resp.Value{resp.StringValue(element)}...) + // If no repeats are allowed, check if the response contains any repeated elements + if !test.allowRepeat { + s := set.NewSet(func() []string { + elements := make([]string, len(res.Array())) + for i, item := range res.Array() { + elements[i] = item.String() + } + return elements + }()) + if s.Cardinality() != len(res.Array()) { + t.Error("response has repeated elements, expected only unique elements.") } - expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) } - if err = client.WriteArray(command); err != nil { + // Check if the resulting set's cardinality is as expected. + if err := client.WriteArray([]resp.Value{resp.StringValue("SCARD"), resp.StringValue(test.key)}); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length \"%d\", got \"%d\"", - len(test.expectedResponse), len(res.Array())) - } - - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.Integer()) { - t.Errorf("unexpected element \"%d\" in response", item.Integer()) - } - } - }) - } -} + if res.Integer() != test.expectedValue { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.key, test.expectedValue, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleSREM", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedValue *set.Set // The final cardinality of the resulting set + expectedResponse int + expectedError error + }{ + { + name: "1. Remove multiple elements and return the number of elements removed", + key: "SremKey1", + presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + command: []string{"SREM", "SremKey1", "one", "two", "three", "nine"}, + expectedValue: set.NewSet([]string{"four", "five", "six", "seven", "eight"}), + expectedResponse: 3, + expectedError: nil, + }, + { + name: "2. If key does not exist, return 0", + key: "SremKey2", + presetValue: nil, + command: []string{"SREM", "SremKey1", "one", "two", "three", "nine"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "3. Return error when the source key is not a set", + key: "SremKey3", + presetValue: "Default value", + command: []string{"SREM", "SremKey3", "one"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: errors.New("value at key SremKey3 is not a set"), + }, + { + name: "4. Command too short", + command: []string{"SREM", "SremKey"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } -func Test_HandleSMOVE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedValues map[string]interface{} - expectedResponse int - expectedError error - }{ - { - name: "1. Return 1 after a successful move of a member from source set to destination set", - presetValues: map[string]interface{}{ - "SmoveSource1": set.NewSet([]string{"one", "two", "three", "four"}), - "SmoveDestination1": set.NewSet([]string{"five", "six", "seven", "eight"}), - }, - command: []string{"SMOVE", "SmoveSource1", "SmoveDestination1", "four"}, - expectedValues: map[string]interface{}{ - "SmoveSource1": set.NewSet([]string{"one", "two", "three"}), - "SmoveDestination1": set.NewSet([]string{"four", "five", "six", "seven", "eight"}), - }, - expectedResponse: 1, - expectedError: nil, - }, - { - name: "2. Return 0 when trying to move a member from source set to destination set when it doesn't exist in source", - presetValues: map[string]interface{}{ - "SmoveSource2": set.NewSet([]string{"one", "two", "three", "four", "five"}), - "SmoveDestination2": set.NewSet([]string{"five", "six", "seven", "eight"}), - }, - command: []string{"SMOVE", "SmoveSource2", "SmoveDestination2", "six"}, - expectedValues: map[string]interface{}{ - "SmoveSource2": set.NewSet([]string{"one", "two", "three", "four", "five"}), - "SmoveDestination2": set.NewSet([]string{"five", "six", "seven", "eight"}), - }, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "3. Return error when the source key is not a set", - presetValues: map[string]interface{}{ - "SmoveSource3": "Default value", - "SmoveDestination3": set.NewSet([]string{"five", "six", "seven", "eight"}), - }, - command: []string{"SMOVE", "SmoveSource3", "SmoveDestination3", "five"}, - expectedValues: map[string]interface{}{ - "SmoveSource3": "Default value", - "SmoveDestination3": set.NewSet([]string{"five", "six", "seven", "eight"}), - }, - expectedResponse: 0, - expectedError: errors.New("source is not a set"), - }, - { - name: "4. Return error when the destination key is not a set", - presetValues: map[string]interface{}{ - "SmoveSource4": set.NewSet([]string{"one", "two", "three", "four", "five"}), - "SmoveDestination4": "Default value", - }, - command: []string{"SMOVE", "SmoveSource4", "SmoveDestination4", "five"}, - expectedValues: map[string]interface{}{ - "SmoveSource4": set.NewSet([]string{"one", "two", "three", "four", "five"}), - "SmoveDestination4": "Default value", - }, - expectedResponse: 0, - expectedError: errors.New("destination is not a set"), - }, - { - name: "5. Command too short", - presetValues: nil, - command: []string{"SMOVE", "SmoveSource5", "SmoveSource6"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - presetValues: nil, - command: []string{"SMOVE", "SmoveSource5", "SmoveSource6", "member1", "member2"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { + switch test.presetValue.(type) { case string: command = []resp.Value{ resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), } expected = "ok" case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} - for _, element := range value.(*set.Set).GetAll() { + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} + for _, element := range test.presetValue.(*set.Set).GetAll() { command = append(command, []resp.Value{resp.StringValue(element)}...) } - expected = strconv.Itoa(value.(*set.Set).Cardinality()) + expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) } if err = client.WriteArray(command); err != nil { @@ -1593,156 +2118,10 @@ func Test_HandleSMOVE(t *testing.T) { t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - - // Check if the resulting set(s) contain the expected members. - if test.expectedValues == nil { - return - } - - for key, value := range test.expectedValues { - switch value.(type) { - case string: - if err := client.WriteArray([]resp.Value{resp.StringValue("GET"), resp.StringValue(key)}); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - if res.String() != value.(string) { - t.Errorf("expected value at key \"%s\" to be \"%s\", got \"%s\"", key, value.(string), res.String()) - } - case *set.Set: - if err := client.WriteArray([]resp.Value{ - resp.StringValue("SMEMBERS"), - resp.StringValue(key), - }); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != value.(*set.Set).Cardinality() { - t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", - key, value.(*set.Set).Cardinality(), len(res.Array())) - } - - for _, item := range res.Array() { - if !value.(*set.Set).Contains(item.String()) { - t.Errorf("unexpected memeber \"%s\", in response", item.String()) - } - } - } - } - }) - } -} - -func Test_HandleSPOP(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedValue int // The final cardinality of the resulting set - expectedResponse []string - expectedError error - }{ - { - name: "1. Return multiple popped elements and modify the set", - key: "SpopKey1", - presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - command: []string{"SPOP", "SpopKey1", "3"}, - expectedValue: 5, - expectedResponse: []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}, - expectedError: nil, - }, - { - name: "2. Return error when the source key is not a set", - key: "SpopKey2", - presetValue: "Default value", - command: []string{"SPOP", "SpopKey2"}, - expectedValue: 0, - expectedResponse: nil, - expectedError: errors.New("value at SpopKey2 is not a set"), - }, - { - name: "3. Command too short", - presetValue: nil, - command: []string{"SPOP"}, - expectedValue: 0, - expectedResponse: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "4. Command too long", - presetValue: nil, - command: []string{"SPOP", "SpopSource5", "SpopSource6", "member1", "member2"}, - expectedValue: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Throw error when count is not an integer", - presetValue: nil, - command: []string{"SPOP", "SpopKey1", "count"}, - expectedValue: 0, - expectedError: errors.New("count must be an integer"), - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), - } - expected = "ok" - case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} - for _, element := range test.presetValue.(*set.Set).GetAll() { - command = append(command, []resp.Value{resp.StringValue(element)}...) - } - expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -1753,285 +2132,156 @@ func Test_HandleSPOP(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - // Check that each returned element is in the list of expected elements. - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.String()) { - t.Errorf("unexpected element \"%s\" in response", item.String()) - } - } - - // Check if the resulting set's cardinality is as expected. - if err := client.WriteArray([]resp.Value{resp.StringValue("SCARD"), resp.StringValue(test.key)}); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if res.Integer() != test.expectedValue { - t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", - test.key, test.expectedValue, res.Integer()) - } - }) - } -} - -func Test_HandleSRANDMEMBER(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedValue int // The final cardinality of the resulting set - allowRepeat bool - expectedResponse []string - expectedError error - }{ - { - // 1. Return multiple random elements without removing them - // Count is positive, do not allow repeated elements - name: "1. Return multiple random elements without removing them", - key: "SRandMemberKey1", - presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - command: []string{"SRANDMEMBER", "SRandMemberKey1", "3"}, - expectedValue: 8, - allowRepeat: false, - expectedResponse: []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}, - expectedError: nil, - }, - { - // 2. Return multiple random elements without removing them - // Count is negative, so allow repeated numbers - name: "2. Return multiple random elements without removing them", - key: "SRandMemberKey2", - presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - command: []string{"SRANDMEMBER", "SRandMemberKey2", "-5"}, - expectedValue: 8, - allowRepeat: true, - expectedResponse: []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}, - expectedError: nil, - }, - { - name: "3. Return error when the source key is not a set", - key: "SRandMemberKey3", - presetValue: "Default value", - command: []string{"SRANDMEMBER", "SRandMemberKey3"}, - expectedValue: 0, - expectedResponse: []string{}, - expectedError: errors.New("value at SRandMemberKey3 is not a set"), - }, - { - name: "4. Command too short", - command: []string{"SRANDMEMBER"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Command too long", - command: []string{"SRANDMEMBER", "SRandMemberSource5", "SRandMemberSource6", "member1", "member2"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Throw error when count is not an integer", - command: []string{"SRANDMEMBER", "SRandMemberKey1", "count"}, - expectedError: errors.New("count must be an integer"), - }, - } + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), - } - expected = "ok" - case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} - for _, element := range test.presetValue.(*set.Set).GetAll() { - command = append(command, []resp.Value{resp.StringValue(element)}...) - } - expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + // Check if the resulting set(s) contain the expected members. + if test.expectedValue == nil { + return } - if err = client.WriteArray(command); err != nil { + if err := client.WriteArray([]resp.Value{resp.StringValue("SMEMBERS"), resp.StringValue(test.key)}); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - // Check that each returned element is in the list of expected elements. - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.String()) { - t.Errorf("unexpected element \"%s\" in response", item.String()) - } - } - - // If no repeats are allowed, check if the response contains any repeated elements - if !test.allowRepeat { - s := set.NewSet(func() []string { - elements := make([]string, len(res.Array())) - for i, item := range res.Array() { - elements[i] = item.String() - } - return elements - }()) - if s.Cardinality() != len(res.Array()) { - t.Error("response has repeated elements, expected only unique elements.") - } - } - - // Check if the resulting set's cardinality is as expected. - if err := client.WriteArray([]resp.Value{resp.StringValue("SCARD"), resp.StringValue(test.key)}); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if res.Integer() != test.expectedValue { - t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", - test.key, test.expectedValue, res.Integer()) - } - }) - } -} + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.key, test.expectedValue.Cardinality(), len(res.Array())) + } -func Test_HandleSREM(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedValue *set.Set // The final cardinality of the resulting set - expectedResponse int - expectedError error - }{ - { - name: "1. Remove multiple elements and return the number of elements removed", - key: "SremKey1", - presetValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - command: []string{"SREM", "SremKey1", "one", "two", "three", "nine"}, - expectedValue: set.NewSet([]string{"four", "five", "six", "seven", "eight"}), - expectedResponse: 3, - expectedError: nil, - }, - { - name: "2. If key does not exist, return 0", - key: "SremKey2", - presetValue: nil, - command: []string{"SREM", "SremKey1", "one", "two", "three", "nine"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "3. Return error when the source key is not a set", - key: "SremKey3", - presetValue: "Default value", - command: []string{"SREM", "SremKey3", "one"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: errors.New("value at key SremKey3 is not a set"), - }, - { - name: "4. Command too short", - command: []string{"SREM", "SremKey"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + for _, item := range res.Array() { + if !test.expectedValue.Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) + } + } + }) + } + }) + + t.Run("Test_HandleSUNION", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedResponse []string + expectedError error + }{ + { + name: "1. Get the union between 2 sets.", + presetValues: map[string]interface{}{ + "SunionKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), + "SunionKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), + }, + command: []string{"SUNION", "SunionKey1", "SunionKey2"}, + expectedResponse: []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}, + expectedError: nil, + }, + { + name: "2. Get the union between 3 sets.", + presetValues: map[string]interface{}{ + "SunionKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SunionKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), + "SunionKey5": set.NewSet([]string{"one", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SUNION", "SunionKey3", "SunionKey4", "SunionKey5"}, + expectedResponse: []string{ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", + "ten", "eleven", "twelve", "thirty-six", + }, + expectedError: nil, + }, + { + name: "3. Throw an error if any of the provided keys are not sets", + presetValues: map[string]interface{}{ + "SunionKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SunionKey7": "Default value", + "SunionKey8": set.NewSet([]string{"one"}), + }, + command: []string{"SUNION", "SunionKey6", "SunionKey7", "SunionKey8"}, + expectedResponse: nil, + expectedError: errors.New("value at key SunionKey7 is not a set"), + }, + { + name: "4. Throw error any of the keys does not hold a set.", + presetValues: map[string]interface{}{ + "SunionKey9": "Default value", + "SunionKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), + "SunionKey11": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), + }, + command: []string{"SUNION", "SunionKey9", "SunionKey10", "SunionKey11"}, + expectedResponse: nil, + expectedError: errors.New("value at key SunionKey9 is not a set"), + }, + { + name: "6. Command too short", + command: []string{"SUNION"}, + expectedResponse: []string{}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), - } - expected = "ok" - case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(test.key)} - for _, element := range test.presetValue.(*set.Set).GetAll() { - command = append(command, []resp.Value{resp.StringValue(element)}...) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - expected = strconv.Itoa(test.presetValue.(*set.Set).Cardinality()) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -2042,351 +2292,184 @@ func Test_HandleSREM(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length \"%d\", got \"%d\"", + len(test.expectedResponse), len(res.Array())) } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - - // Check if the resulting set(s) contain the expected members. - if test.expectedValue == nil { - return - } - - if err := client.WriteArray([]resp.Value{resp.StringValue("SMEMBERS"), resp.StringValue(test.key)}); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - if len(res.Array()) != test.expectedValue.Cardinality() { - t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", - test.key, test.expectedValue.Cardinality(), len(res.Array())) - } - - for _, item := range res.Array() { - if !test.expectedValue.Contains(item.String()) { - t.Errorf("unexpected memeber \"%s\", in response", item.String()) + for _, item := range res.Array() { + if !slices.Contains(test.expectedResponse, item.String()) { + t.Errorf("unexpected element \"%s\" in response", item.String()) + } } - } - }) - } -} - -func Test_HandleSUNION(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedResponse []string - expectedError error - }{ - { - name: "1. Get the union between 2 sets.", - presetValues: map[string]interface{}{ - "SunionKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), - "SunionKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), - }, - command: []string{"SUNION", "SunionKey1", "SunionKey2"}, - expectedResponse: []string{"one", "two", "three", "four", "five", "six", "seven", "eight"}, - expectedError: nil, - }, - { - name: "2. Get the union between 3 sets.", - presetValues: map[string]interface{}{ - "SunionKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SunionKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), - "SunionKey5": set.NewSet([]string{"one", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SUNION", "SunionKey3", "SunionKey4", "SunionKey5"}, - expectedResponse: []string{ - "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", - "ten", "eleven", "twelve", "thirty-six", - }, - expectedError: nil, - }, - { - name: "3. Throw an error if any of the provided keys are not sets", - presetValues: map[string]interface{}{ - "SunionKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SunionKey7": "Default value", - "SunionKey8": set.NewSet([]string{"one"}), - }, - command: []string{"SUNION", "SunionKey6", "SunionKey7", "SunionKey8"}, - expectedResponse: nil, - expectedError: errors.New("value at key SunionKey7 is not a set"), - }, - { - name: "4. Throw error any of the keys does not hold a set.", - presetValues: map[string]interface{}{ - "SunionKey9": "Default value", - "SunionKey10": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven"}), - "SunionKey11": set.NewSet([]string{"seven", "eight", "nine", "ten", "twelve"}), - }, - command: []string{"SUNION", "SunionKey9", "SunionKey10", "SunionKey11"}, - expectedResponse: nil, - expectedError: errors.New("value at key SunionKey9 is not a set"), - }, - { - name: "6. Command too short", - command: []string{"SUNION"}, - expectedResponse: []string{}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + }) + } + }) + + t.Run("Test_HandleSUNIONSTORE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + destination string + command []string + expectedValue *set.Set + expectedResponse int + expectedError error + }{ + { + name: "1. Get the intersection between 2 sets and store it at the destination.", + presetValues: map[string]interface{}{ + "SunionStoreKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), + "SunionStoreKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), + }, + destination: "SunionStoreDestination1", + command: []string{"SUNIONSTORE", "SunionStoreDestination1", "SunionStoreKey1", "SunionStoreKey2"}, + expectedValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + expectedResponse: 8, + expectedError: nil, + }, + { + name: "2. Get the intersection between 3 sets and store it at the destination key.", + presetValues: map[string]interface{}{ + "SunionStoreKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SunionStoreKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), + "SunionStoreKey5": set.NewSet([]string{"one", "seven", "eight", "nine", "ten", "twelve"}), + }, + destination: "SunionStoreDestination2", + command: []string{"SUNIONSTORE", "SunionStoreDestination2", "SunionStoreKey3", "SunionStoreKey4", "SunionStoreKey5"}, + expectedValue: set.NewSet([]string{ + "one", "two", "three", "four", "five", "six", "seven", "eight", + "nine", "ten", "eleven", "twelve", "thirty-six", + }), + expectedResponse: 13, + expectedError: nil, + }, + { + name: "3. Throw error when any of the keys is not a set", + presetValues: map[string]interface{}{ + "SunionStoreKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), + "SunionStoreKey7": "Default value", + "SunionStoreKey8": set.NewSet([]string{"one"}), + }, + destination: "SunionStoreDestination3", + command: []string{"SUNIONSTORE", "SunionStoreDestination3", "SunionStoreKey6", "SunionStoreKey7", "SunionStoreKey8"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: errors.New("value at key SunionStoreKey7 is not a set"), + }, + { + name: "5. Command too short", + command: []string{"SUNIONSTORE", "SunionStoreDestination6"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *set.Set: + command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} + for _, element := range value.(*set.Set).GetAll() { + command = append(command, []resp.Value{resp.StringValue(element)}...) + } + expected = strconv.Itoa(value.(*set.Set).Cardinality()) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} - for _, element := range value.(*set.Set).GetAll() { - command = append(command, []resp.Value{resp.StringValue(element)}...) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = strconv.Itoa(value.(*set.Set).Cardinality()) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - return - } - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length \"%d\", got \"%d\"", - len(test.expectedResponse), len(res.Array())) - } + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } - for _, item := range res.Array() { - if !slices.Contains(test.expectedResponse, item.String()) { - t.Errorf("unexpected element \"%s\" in response", item.String()) + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) } - } - }) - } -} -func Test_HandleSUNIONSTORE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - destination string - command []string - expectedValue *set.Set - expectedResponse int - expectedError error - }{ - { - name: "1. Get the intersection between 2 sets and store it at the destination.", - presetValues: map[string]interface{}{ - "SunionStoreKey1": set.NewSet([]string{"one", "two", "three", "four", "five"}), - "SunionStoreKey2": set.NewSet([]string{"three", "four", "five", "six", "seven", "eight"}), - }, - destination: "SunionStoreDestination1", - command: []string{"SUNIONSTORE", "SunionStoreDestination1", "SunionStoreKey1", "SunionStoreKey2"}, - expectedValue: set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - expectedResponse: 8, - expectedError: nil, - }, - { - name: "2. Get the intersection between 3 sets and store it at the destination key.", - presetValues: map[string]interface{}{ - "SunionStoreKey3": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SunionStoreKey4": set.NewSet([]string{"one", "two", "thirty-six", "twelve", "eleven", "eight"}), - "SunionStoreKey5": set.NewSet([]string{"one", "seven", "eight", "nine", "ten", "twelve"}), - }, - destination: "SunionStoreDestination2", - command: []string{"SUNIONSTORE", "SunionStoreDestination2", "SunionStoreKey3", "SunionStoreKey4", "SunionStoreKey5"}, - expectedValue: set.NewSet([]string{ - "one", "two", "three", "four", "five", "six", "seven", "eight", - "nine", "ten", "eleven", "twelve", "thirty-six", - }), - expectedResponse: 13, - expectedError: nil, - }, - { - name: "3. Throw error when any of the keys is not a set", - presetValues: map[string]interface{}{ - "SunionStoreKey6": set.NewSet([]string{"one", "two", "three", "four", "five", "six", "seven", "eight"}), - "SunionStoreKey7": "Default value", - "SunionStoreKey8": set.NewSet([]string{"one"}), - }, - destination: "SunionStoreDestination3", - command: []string{"SUNIONSTORE", "SunionStoreDestination3", "SunionStoreKey6", "SunionStoreKey7", "SunionStoreKey8"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: errors.New("value at key SunionStoreKey7 is not a set"), - }, - { - name: "5. Command too short", - command: []string{"SUNIONSTORE", "SunionStoreDestination6"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + // Check if the resulting set(s) contain the expected members. + if test.expectedValue == nil { + return + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), - } - expected = "ok" - case *set.Set: - command = []resp.Value{resp.StringValue("SADD"), resp.StringValue(key)} - for _, element := range value.(*set.Set).GetAll() { - command = append(command, []resp.Value{resp.StringValue(element)}...) - } - expected = strconv.Itoa(value.(*set.Set).Cardinality()) - } + if err := client.WriteArray([]resp.Value{ + resp.StringValue("SMEMBERS"), + resp.StringValue(test.destination), + }); err != nil { + t.Error(err) + } + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", + test.destination, test.expectedValue.Cardinality(), len(res.Array())) + } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + for _, item := range res.Array() { + if !test.expectedValue.Contains(item.String()) { + t.Errorf("unexpected memeber \"%s\", in response", item.String()) } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - - // Check if the resulting set(s) contain the expected members. - if test.expectedValue == nil { - return - } - - if err := client.WriteArray([]resp.Value{ - resp.StringValue("SMEMBERS"), - resp.StringValue(test.destination), - }); err != nil { - t.Error(err) - } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != test.expectedValue.Cardinality() { - t.Errorf("expected set at key \"%s\" to have cardinality %d, got %d", - test.destination, test.expectedValue.Cardinality(), len(res.Array())) - } - - for _, item := range res.Array() { - if !test.expectedValue.Contains(item.String()) { - t.Errorf("unexpected memeber \"%s\", in response", item.String()) - } - } - }) - } + }) + } + }) } diff --git a/internal/modules/sorted_set/commands_test.go b/internal/modules/sorted_set/commands_test.go index 8864c4f6..1d6dd5d5 100644 --- a/internal/modules/sorted_set/commands_test.go +++ b/internal/modules/sorted_set/commands_test.go @@ -16,7 +16,6 @@ package sorted_set_test import ( "errors" - "fmt" "github.com/echovault/echovault/echovault" "github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal/config" @@ -24,206 +23,232 @@ import ( "github.com/echovault/echovault/internal/modules/sorted_set" "github.com/tidwall/resp" "math" - "net" "slices" "strconv" "strings" - "sync" "testing" ) -var mockServer *echovault.EchoVault -var addr = "localhost" -var port int +func Test_SortedSet(t *testing.T) { + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } -func init() { - port, _ = internal.GetFreePort() - mockServer, _ = echovault.NewEchoVault( + mockServer, err := echovault.NewEchoVault( echovault.WithConfig(config.Config{ - BindAddr: addr, + BindAddr: "localhost", Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) - wg := sync.WaitGroup{} - wg.Add(1) + if err != nil { + t.Error(err) + return + } + go func() { - wg.Done() mockServer.Start() }() - wg.Wait() -} -func Test_HandleZADD(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValue *sorted_set.SortedSet - key string - command []string - expectedResponse int - expectedError error - }{ - { - name: "1. Create new sorted set and return the cardinality of the new sorted set", - presetValue: nil, - key: "ZaddKey1", - command: []string{"ZADD", "ZaddKey1", "5.5", "member1", "67.77", "member2", "10", "member3", "-inf", "member4", "+inf", "member5"}, - expectedResponse: 5, - expectedError: nil, - }, - { - name: "2. Only add the elements that do not currently exist in the sorted set when NX flag is provided", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), - key: "ZaddKey2", - command: []string{"ZADD", "ZaddKey2", "NX", "5.5", "member1", "67.77", "member4", "10", "member5"}, - expectedResponse: 2, - expectedError: nil, - }, - { - name: "3. Do not add any elements when providing existing members with NX flag", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), - key: "ZaddKey3", - command: []string{"ZADD", "ZaddKey3", "NX", "5.5", "member1", "67.77", "member2", "10", "member3"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "4. Successfully add elements to an existing set when XX flag is provided with existing elements", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), - key: "ZaddKey4", - command: []string{"ZADD", "ZaddKey4", "XX", "CH", "55", "member1", "1005", "member2", "15", "member3", "99.75", "member4"}, - expectedResponse: 3, - expectedError: nil, - }, - { - name: "5. Fail to add element when providing XX flag with elements that do not exist in the sorted set.", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), - key: "ZaddKey5", - command: []string{"ZADD", "ZaddKey5", "XX", "5.5", "member4", "100.5", "member5", "15", "member6"}, - expectedResponse: 0, - expectedError: nil, - }, - { - // 6. Only update the elements where provided score is greater than current score and GT flag is provided - // Return only the new elements added by default - name: "6. Only update the elements where provided score is greater than current score and GT flag is provided", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), - key: "ZaddKey6", - command: []string{"ZADD", "ZaddKey6", "XX", "CH", "GT", "7.5", "member1", "100.5", "member4", "15", "member5"}, - expectedResponse: 1, - expectedError: nil, - }, - { - // 7. Only update the elements where provided score is less than current score if LT flag is provided - // Return only the new elements added by default. - name: "7. Only update the elements where provided score is less than current score if LT flag is provided", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), - key: "ZaddKey7", - command: []string{"ZADD", "ZaddKey7", "XX", "LT", "3.5", "member1", "100.5", "member4", "15", "member5"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "8. Return all the elements that were updated AND added when CH flag is provided", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), - key: "ZaddKey8", - command: []string{"ZADD", "ZaddKey8", "XX", "LT", "CH", "3.5", "member1", "100.5", "member4", "15", "member5"}, - expectedResponse: 1, - expectedError: nil, - }, - { - name: "9. Increment the member by score", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), - key: "ZaddKey9", - command: []string{"ZADD", "ZaddKey9", "INCR", "5.5", "member3"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "10. Fail when GT/LT flag is provided alongside NX flag", - presetValue: nil, - key: "ZaddKey10", - command: []string{"ZADD", "ZaddKey10", "NX", "LT", "CH", "3.5", "member1", "100.5", "member4", "15", "member5"}, - expectedResponse: 0, - expectedError: errors.New("GT/LT flags not allowed if NX flag is provided"), - }, - { - name: "11. Command is too short", - presetValue: nil, - key: "ZaddKey11", - command: []string{"ZADD", "ZaddKey11"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "12. Throw error when score/member entries are do not match", - presetValue: nil, - key: "ZaddKey11", - command: []string{"ZADD", "ZaddKey12", "10.5", "member1", "12.5"}, - expectedResponse: 0, - expectedError: errors.New("score/member pairs must be float/string"), - }, - { - name: "13. Throw error when INCR flag is passed with more than one score/member pair", - presetValue: nil, - key: "ZaddKey13", - command: []string{"ZADD", "ZaddKey13", "INCR", "10.5", "member1", "12.5", "member2"}, - expectedResponse: 0, - expectedError: errors.New("cannot pass more than one score/member pair when INCR flag is provided"), - }, - } + t.Cleanup(func() { + mockServer.ShutDown() + }) + + t.Run("Test_HandleZADD", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValue *sorted_set.SortedSet + key string + command []string + expectedResponse int + expectedError error + }{ + { + name: "1. Create new sorted set and return the cardinality of the new sorted set", + presetValue: nil, + key: "ZaddKey1", + command: []string{"ZADD", "ZaddKey1", "5.5", "member1", "67.77", "member2", "10", "member3", "-inf", "member4", "+inf", "member5"}, + expectedResponse: 5, + expectedError: nil, + }, + { + name: "2. Only add the elements that do not currently exist in the sorted set when NX flag is provided", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + }), + key: "ZaddKey2", + command: []string{"ZADD", "ZaddKey2", "NX", "5.5", "member1", "67.77", "member4", "10", "member5"}, + expectedResponse: 2, + expectedError: nil, + }, + { + name: "3. Do not add any elements when providing existing members with NX flag", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + }), + key: "ZaddKey3", + command: []string{"ZADD", "ZaddKey3", "NX", "5.5", "member1", "67.77", "member2", "10", "member3"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "4. Successfully add elements to an existing set when XX flag is provided with existing elements", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + }), + key: "ZaddKey4", + command: []string{"ZADD", "ZaddKey4", "XX", "CH", "55", "member1", "1005", "member2", "15", "member3", "99.75", "member4"}, + expectedResponse: 3, + expectedError: nil, + }, + { + name: "5. Fail to add element when providing XX flag with elements that do not exist in the sorted set.", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + }), + key: "ZaddKey5", + command: []string{"ZADD", "ZaddKey5", "XX", "5.5", "member4", "100.5", "member5", "15", "member6"}, + expectedResponse: 0, + expectedError: nil, + }, + { + // 6. Only update the elements where provided score is greater than current score and GT flag is provided + // Return only the new elements added by default + name: "6. Only update the elements where provided score is greater than current score and GT flag is provided", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + }), + key: "ZaddKey6", + command: []string{"ZADD", "ZaddKey6", "XX", "CH", "GT", "7.5", "member1", "100.5", "member4", "15", "member5"}, + expectedResponse: 1, + expectedError: nil, + }, + { + // 7. Only update the elements where provided score is less than current score if LT flag is provided + // Return only the new elements added by default. + name: "7. Only update the elements where provided score is less than current score if LT flag is provided", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + }), + key: "ZaddKey7", + command: []string{"ZADD", "ZaddKey7", "XX", "LT", "3.5", "member1", "100.5", "member4", "15", "member5"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "8. Return all the elements that were updated AND added when CH flag is provided", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + }), + key: "ZaddKey8", + command: []string{"ZADD", "ZaddKey8", "XX", "LT", "CH", "3.5", "member1", "100.5", "member4", "15", "member5"}, + expectedResponse: 1, + expectedError: nil, + }, + { + name: "9. Increment the member by score", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + }), + key: "ZaddKey9", + command: []string{"ZADD", "ZaddKey9", "INCR", "5.5", "member3"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "10. Fail when GT/LT flag is provided alongside NX flag", + presetValue: nil, + key: "ZaddKey10", + command: []string{"ZADD", "ZaddKey10", "NX", "LT", "CH", "3.5", "member1", "100.5", "member4", "15", "member5"}, + expectedResponse: 0, + expectedError: errors.New("GT/LT flags not allowed if NX flag is provided"), + }, + { + name: "11. Command is too short", + presetValue: nil, + key: "ZaddKey11", + command: []string{"ZADD", "ZaddKey11"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "12. Throw error when score/member entries are do not match", + presetValue: nil, + key: "ZaddKey11", + command: []string{"ZADD", "ZaddKey12", "10.5", "member1", "12.5"}, + expectedResponse: 0, + expectedError: errors.New("score/member pairs must be float/string"), + }, + { + name: "13. Throw error when INCR flag is passed with more than one score/member pair", + presetValue: nil, + key: "ZaddKey13", + command: []string{"ZADD", "ZaddKey13", "INCR", "10.5", "member1", "12.5", "member2"}, + expectedResponse: 0, + expectedError: errors.New("cannot pass more than one score/member pair when INCR flag is provided"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if res.Integer() != test.presetValue.Cardinality() { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} - for _, member := range test.presetValue.GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -234,122 +259,302 @@ func Test_HandleZADD(t *testing.T) { t.Error(err) } - if res.Integer() != test.presetValue.Cardinality() { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleZCARD", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValue interface{} + key string + command []string + expectedResponse int + expectedError error + }{ + { + name: "1. Get cardinality of valid sorted set.", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + }), + key: "ZcardKey1", + command: []string{"ZCARD", "ZcardKey1"}, + expectedResponse: 3, + expectedError: nil, + }, + { + name: "2. Return 0 when trying to get cardinality from non-existent key", + presetValue: nil, + key: "ZcardKey2", + command: []string{"ZCARD", "ZcardKey2"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "3. Command is too short", + presetValue: nil, + key: "ZcardKey3", + command: []string{"ZCARD"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "4. Command too long", + presetValue: nil, + key: "ZcardKey4", + command: []string{"ZCARD", "ZcardKey4", "ZcardKey5"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Return error when not a sorted set", + presetValue: "Default value", + key: "ZcardKey5", + command: []string{"ZCARD", "ZcardKey5"}, + expectedResponse: 0, + expectedError: errors.New("value at ZcardKey5 is not a sorted set"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - }) - } -} + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } -func Test_HandleZCARD(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValue interface{} - key string - command []string - expectedResponse int - expectedError error - }{ - { - name: "1. Get cardinality of valid sorted set.", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - }), - key: "ZcardKey1", - command: []string{"ZCARD", "ZcardKey1"}, - expectedResponse: 3, - expectedError: nil, - }, - { - name: "2. Return 0 when trying to get cardinality from non-existent key", - presetValue: nil, - key: "ZcardKey2", - command: []string{"ZCARD", "ZcardKey2"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "3. Command is too short", - presetValue: nil, - key: "ZcardKey3", - command: []string{"ZCARD"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "4. Command too long", - presetValue: nil, - key: "ZcardKey4", - command: []string{"ZCARD", "ZcardKey4", "ZcardKey5"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Return error when not a sorted set", - presetValue: "Default value", - key: "ZcardKey5", - command: []string{"ZCARD", "ZcardKey5"}, - expectedResponse: 0, - expectedError: errors.New("value at ZcardKey5 is not a sorted set"), - }, - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} - for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleZCOUNT", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValue interface{} + key string + command []string + expectedResponse int + expectedError error + }{ + { + name: "1. Get entire count using infinity boundaries", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + {Value: "member4", Score: sorted_set.Score(1083.13)}, + {Value: "member5", Score: sorted_set.Score(11)}, + {Value: "member6", Score: sorted_set.Score(math.Inf(-1))}, + {Value: "member7", Score: sorted_set.Score(math.Inf(1))}, + }), + key: "ZcountKey1", + command: []string{"ZCOUNT", "ZcountKey1", "-inf", "+inf"}, + expectedResponse: 7, + expectedError: nil, + }, + { + name: "2. Get count of sub-set from -inf to limit", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + {Value: "member4", Score: sorted_set.Score(1083.13)}, + {Value: "member5", Score: sorted_set.Score(11)}, + {Value: "member6", Score: sorted_set.Score(math.Inf(-1))}, + {Value: "member7", Score: sorted_set.Score(math.Inf(1))}, + }), + key: "ZcountKey2", + command: []string{"ZCOUNT", "ZcountKey2", "-inf", "90"}, + expectedResponse: 5, + expectedError: nil, + }, + { + name: "3. Get count of sub-set from bottom boundary to +inf limit", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "member1", Score: sorted_set.Score(5.5)}, + {Value: "member2", Score: sorted_set.Score(67.77)}, + {Value: "member3", Score: sorted_set.Score(10)}, + {Value: "member4", Score: sorted_set.Score(1083.13)}, + {Value: "member5", Score: sorted_set.Score(11)}, + {Value: "member6", Score: sorted_set.Score(math.Inf(-1))}, + {Value: "member7", Score: sorted_set.Score(math.Inf(1))}, + }), + key: "ZcountKey3", + command: []string{"ZCOUNT", "ZcountKey3", "1000", "+inf"}, + expectedResponse: 2, + expectedError: nil, + }, + { + name: "4. Return error when bottom boundary is not a valid double/float", + presetValue: nil, + key: "ZcountKey4", + command: []string{"ZCOUNT", "ZcountKey4", "min", "10"}, + expectedResponse: 0, + expectedError: errors.New("min constraint must be a double"), + }, + { + name: "5. Return error when top boundary is not a valid double/float", + presetValue: nil, + key: "ZcountKey5", + command: []string{"ZCOUNT", "ZcountKey5", "-10", "max"}, + expectedResponse: 0, + expectedError: errors.New("max constraint must be a double"), + }, + { + name: "6. Command is too short", + presetValue: nil, + key: "ZcountKey6", + command: []string{"ZCOUNT"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "7. Command too long", + presetValue: nil, + key: "ZcountKey7", + command: []string{"ZCOUNT", "ZcountKey4", "min", "max", "count"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "8. Throw error when value at the key is not a sorted set", + presetValue: "Default value", + key: "ZcountKey8", + command: []string{"ZCOUNT", "ZcountKey8", "1", "10"}, + expectedResponse: 0, + expectedError: errors.New("value at ZcountKey8 is not a sorted set"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -360,166 +565,345 @@ func Test_HandleZCARD(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleZLEXCOUNT", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValue interface{} + key string + command []string + expectedResponse int + expectedError error + }{ + { + name: "1. Get entire count using infinity boundaries", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "e", Score: sorted_set.Score(1)}, + {Value: "f", Score: sorted_set.Score(1)}, + {Value: "g", Score: sorted_set.Score(1)}, + {Value: "h", Score: sorted_set.Score(1)}, + {Value: "i", Score: sorted_set.Score(1)}, + {Value: "j", Score: sorted_set.Score(1)}, + {Value: "k", Score: sorted_set.Score(1)}, + }), + key: "ZlexCountKey1", + command: []string{"ZLEXCOUNT", "ZlexCountKey1", "f", "j"}, + expectedResponse: 5, + expectedError: nil, + }, + { + name: "2. Return 0 when the members do not have the same score", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: sorted_set.Score(5.5)}, + {Value: "b", Score: sorted_set.Score(67.77)}, + {Value: "c", Score: sorted_set.Score(10)}, + {Value: "d", Score: sorted_set.Score(1083.13)}, + {Value: "e", Score: sorted_set.Score(11)}, + {Value: "f", Score: sorted_set.Score(math.Inf(-1))}, + {Value: "g", Score: sorted_set.Score(math.Inf(1))}, + }), + key: "ZlexCountKey2", + command: []string{"ZLEXCOUNT", "ZlexCountKey2", "a", "b"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "3. Return 0 when the key does not exist", + presetValue: nil, + key: "ZlexCountKey3", + command: []string{"ZLEXCOUNT", "ZlexCountKey3", "a", "z"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "4. Return error when the value at the key is not a sorted set", + presetValue: "Default value", + key: "ZlexCountKey4", + command: []string{"ZLEXCOUNT", "ZlexCountKey4", "a", "z"}, + expectedResponse: 0, + expectedError: errors.New("value at ZlexCountKey4 is not a sorted set"), + }, + { + name: "5. Command is too short", + presetValue: nil, + key: "ZlexCountKey5", + command: []string{"ZLEXCOUNT"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + presetValue: nil, + key: "ZlexCountKey6", + command: []string{"ZLEXCOUNT", "ZlexCountKey6", "min", "max", "count"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - }) - } -} + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } -func Test_HandleZCOUNT(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValue interface{} - key string - command []string - expectedResponse int - expectedError error - }{ - { - name: "1. Get entire count using infinity boundaries", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - {Value: "member4", Score: sorted_set.Score(1083.13)}, - {Value: "member5", Score: sorted_set.Score(11)}, - {Value: "member6", Score: sorted_set.Score(math.Inf(-1))}, - {Value: "member7", Score: sorted_set.Score(math.Inf(1))}, - }), - key: "ZcountKey1", - command: []string{"ZCOUNT", "ZcountKey1", "-inf", "+inf"}, - expectedResponse: 7, - expectedError: nil, - }, - { - name: "2. Get count of sub-set from -inf to limit", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - {Value: "member4", Score: sorted_set.Score(1083.13)}, - {Value: "member5", Score: sorted_set.Score(11)}, - {Value: "member6", Score: sorted_set.Score(math.Inf(-1))}, - {Value: "member7", Score: sorted_set.Score(math.Inf(1))}, - }), - key: "ZcountKey2", - command: []string{"ZCOUNT", "ZcountKey2", "-inf", "90"}, - expectedResponse: 5, - expectedError: nil, - }, - { - name: "3. Get count of sub-set from bottom boundary to +inf limit", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "member1", Score: sorted_set.Score(5.5)}, - {Value: "member2", Score: sorted_set.Score(67.77)}, - {Value: "member3", Score: sorted_set.Score(10)}, - {Value: "member4", Score: sorted_set.Score(1083.13)}, - {Value: "member5", Score: sorted_set.Score(11)}, - {Value: "member6", Score: sorted_set.Score(math.Inf(-1))}, - {Value: "member7", Score: sorted_set.Score(math.Inf(1))}, - }), - key: "ZcountKey3", - command: []string{"ZCOUNT", "ZcountKey3", "1000", "+inf"}, - expectedResponse: 2, - expectedError: nil, - }, - { - name: "4. Return error when bottom boundary is not a valid double/float", - presetValue: nil, - key: "ZcountKey4", - command: []string{"ZCOUNT", "ZcountKey4", "min", "10"}, - expectedResponse: 0, - expectedError: errors.New("min constraint must be a double"), - }, - { - name: "5. Return error when top boundary is not a valid double/float", - presetValue: nil, - key: "ZcountKey5", - command: []string{"ZCOUNT", "ZcountKey5", "-10", "max"}, - expectedResponse: 0, - expectedError: errors.New("max constraint must be a double"), - }, - { - name: "6. Command is too short", - presetValue: nil, - key: "ZcountKey6", - command: []string{"ZCOUNT"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "7. Command too long", - presetValue: nil, - key: "ZcountKey7", - command: []string{"ZCOUNT", "ZcountKey4", "min", "max", "count"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "8. Throw error when value at the key is not a sorted set", - presetValue: "Default value", - key: "ZcountKey8", - command: []string{"ZCOUNT", "ZcountKey8", "1", "10"}, - expectedResponse: 0, - expectedError: errors.New("value at ZcountKey8 is not a sorted set"), - }, - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} - for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + }) + } + }) + + t.Run("Test_HandleZDIFF", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedResponse [][]string + expectedError error + }{ + { + name: "1. Get the difference between 2 sorted sets without scores.", + presetValues: map[string]interface{}{ + "ZdiffKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, + {Value: "two", Score: 2}, + {Value: "three", Score: 3}, + {Value: "four", Score: 4}, + }), + "ZdiffKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 3}, + {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, + {Value: "eight", Score: 8}, + }), + }, + command: []string{"ZDIFF", "ZdiffKey1", "ZdiffKey2"}, + expectedResponse: [][]string{{"one"}, {"two"}}, + expectedError: nil, + }, + { + name: "2. Get the difference between 2 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZdiffKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, + {Value: "two", Score: 2}, + {Value: "three", Score: 3}, + {Value: "four", Score: 4}, + }), + "ZdiffKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 3}, + {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, + {Value: "eight", Score: 8}, + }), + }, + command: []string{"ZDIFF", "ZdiffKey3", "ZdiffKey4", "WITHSCORES"}, + expectedResponse: [][]string{{"one", "1"}, {"two", "2"}}, + expectedError: nil, + }, + { + name: "3. Get the difference between 3 sets with scores.", + presetValues: map[string]interface{}{ + "ZdiffKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZdiffKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, + }), + "ZdiffKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZDIFF", "ZdiffKey5", "ZdiffKey6", "ZdiffKey7", "WITHSCORES"}, + expectedResponse: [][]string{{"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}}, + expectedError: nil, + }, + { + name: "4. Return sorted set if only one key exists and is a sorted set", + presetValues: map[string]interface{}{ + "ZdiffKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + command: []string{"ZDIFF", "ZdiffKey8", "ZdiffKey9", "ZdiffKey10", "WITHSCORES"}, + expectedResponse: [][]string{ + {"one", "1"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, + {"six", "6"}, {"seven", "7"}, {"eight", "8"}, + }, + expectedError: nil, + }, + { + name: "5. Throw error when one of the keys is not a sorted set.", + presetValues: map[string]interface{}{ + "ZdiffKey11": "Default value", + "ZdiffKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, + }), + "ZdiffKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZDIFF", "ZdiffKey11", "ZdiffKey12", "ZdiffKey13"}, + expectedResponse: nil, + expectedError: errors.New("value at ZdiffKey11 is not a sorted set"), + }, + { + name: "6. Command too short", + command: []string{"ZDIFF"}, + expectedResponse: [][]string{}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -530,142 +914,224 @@ func Test_HandleZCOUNT(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - }) - } -} + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() + } + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) + } + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) + } + } + } + } + }) + } + }) + + t.Run("Test_HandleZDIFFSTORE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + destination string + command []string + expectedValue *sorted_set.SortedSet + expectedResponse int + expectedError error + }{ + { + name: "1. Get the difference between 2 sorted sets.", + presetValues: map[string]interface{}{ + "ZdiffStoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + "ZdiffStoreKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + destination: "ZdiffStoreDestinationKey1", + command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey1", "ZdiffStoreKey1", "ZdiffStoreKey2"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}, {Value: "two", Score: 2}}), + expectedResponse: 2, + expectedError: nil, + }, + { + name: "2. Get the difference between 3 sorted sets.", + presetValues: map[string]interface{}{ + "ZdiffStoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZdiffStoreKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, + }), + "ZdiffStoreKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZdiffStoreDestinationKey2", + command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey2", "ZdiffStoreKey3", "ZdiffStoreKey4", "ZdiffStoreKey5"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + }), + expectedResponse: 4, + expectedError: nil, + }, + { + name: "3. Return base sorted set element if base set is the only existing key provided and is a valid sorted set", + presetValues: map[string]interface{}{ + "ZdiffStoreKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + destination: "ZdiffStoreDestinationKey3", + command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey3", "ZdiffStoreKey6", "ZdiffStoreKey7", "ZdiffStoreKey8"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + expectedResponse: 8, + expectedError: nil, + }, + { + name: "4. Throw error when base sorted set is not a set.", + presetValues: map[string]interface{}{ + "ZdiffStoreKey9": "Default value", + "ZdiffStoreKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, + }), + "ZdiffStoreKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZdiffStoreDestinationKey4", + command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey4", "ZdiffStoreKey9", "ZdiffStoreKey10", "ZdiffStoreKey11"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: errors.New("value at ZdiffStoreKey9 is not a sorted set"), + }, + { + name: "5. Return 0 when base set is non-existent.", + destination: "ZdiffStoreDestinationKey5", + presetValues: map[string]interface{}{ + "ZdiffStoreKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, + }), + "ZdiffStoreKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey5", "non-existent", "ZdiffStoreKey12", "ZdiffStoreKey13"}, + expectedValue: nil, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "6. Command too short", + command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey6"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } -func Test_HandleZLEXCOUNT(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValue interface{} - key string - command []string - expectedResponse int - expectedError error - }{ - { - name: "1. Get entire count using infinity boundaries", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "e", Score: sorted_set.Score(1)}, - {Value: "f", Score: sorted_set.Score(1)}, - {Value: "g", Score: sorted_set.Score(1)}, - {Value: "h", Score: sorted_set.Score(1)}, - {Value: "i", Score: sorted_set.Score(1)}, - {Value: "j", Score: sorted_set.Score(1)}, - {Value: "k", Score: sorted_set.Score(1)}, - }), - key: "ZlexCountKey1", - command: []string{"ZLEXCOUNT", "ZlexCountKey1", "f", "j"}, - expectedResponse: 5, - expectedError: nil, - }, - { - name: "2. Return 0 when the members do not have the same score", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: sorted_set.Score(5.5)}, - {Value: "b", Score: sorted_set.Score(67.77)}, - {Value: "c", Score: sorted_set.Score(10)}, - {Value: "d", Score: sorted_set.Score(1083.13)}, - {Value: "e", Score: sorted_set.Score(11)}, - {Value: "f", Score: sorted_set.Score(math.Inf(-1))}, - {Value: "g", Score: sorted_set.Score(math.Inf(1))}, - }), - key: "ZlexCountKey2", - command: []string{"ZLEXCOUNT", "ZlexCountKey2", "a", "b"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "3. Return 0 when the key does not exist", - presetValue: nil, - key: "ZlexCountKey3", - command: []string{"ZLEXCOUNT", "ZlexCountKey3", "a", "z"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "4. Return error when the value at the key is not a sorted set", - presetValue: "Default value", - key: "ZlexCountKey4", - command: []string{"ZLEXCOUNT", "ZlexCountKey4", "a", "z"}, - expectedResponse: 0, - expectedError: errors.New("value at ZlexCountKey4 is not a sorted set"), - }, - { - name: "5. Command is too short", - presetValue: nil, - key: "ZlexCountKey5", - command: []string{"ZLEXCOUNT"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - presetValue: nil, - key: "ZlexCountKey6", - command: []string{"ZLEXCOUNT", "ZlexCountKey6", "min", "max", "count"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), - } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} - for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -676,187 +1142,261 @@ func Test_HandleZLEXCOUNT(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + // Check if the resulting sorted set has the expected members/scores + if test.expectedValue == nil { + return + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(test.destination), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - }) - } -} + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + test.destination, test.expectedValue.Cardinality(), len(res.Array())) + } -func Test_HandleZDIFF(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedResponse [][]string - expectedError error - }{ - { - name: "1. Get the difference between 2 sorted sets without scores.", - presetValues: map[string]interface{}{ - "ZdiffKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, - {Value: "two", Score: 2}, - {Value: "three", Score: 3}, - {Value: "four", Score: 4}, + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !test.expectedValue.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) + } + if test.expectedValue.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) + } + } + }) + } + }) + + t.Run("Test_HandleZINCRBY", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValue interface{} + key string + command []string + expectedValue *sorted_set.SortedSet + expectedResponse string + expectedError error + }{ + { + name: "1. Successfully increment by int. Return the new score", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, }), - "ZdiffKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, - {Value: "four", Score: 4}, + key: "ZincrbyKey1", + command: []string{"ZINCRBY", "ZincrbyKey1", "5", "one"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 6}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, - {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, - {Value: "eight", Score: 8}, }), + expectedResponse: "6", + expectedError: nil, }, - command: []string{"ZDIFF", "ZdiffKey1", "ZdiffKey2"}, - expectedResponse: [][]string{{"one"}, {"two"}}, - expectedError: nil, - }, - { - name: "2. Get the difference between 2 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZdiffKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, - {Value: "two", Score: 2}, - {Value: "three", Score: 3}, - {Value: "four", Score: 4}, + { + name: "2. Successfully increment by float. Return new score", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, }), - "ZdiffKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, - {Value: "four", Score: 4}, + key: "ZincrbyKey2", + command: []string{"ZINCRBY", "ZincrbyKey2", "346.785", "one"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 347.785}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, - {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, - {Value: "eight", Score: 8}, }), + expectedResponse: "347.785", + expectedError: nil, + }, + { + name: "3. Increment on non-existent sorted set will create the set with the member and increment as its score", + presetValue: nil, + key: "ZincrbyKey3", + command: []string{"ZINCRBY", "ZincrbyKey3", "346.785", "one"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 346.785}, + }), + expectedResponse: "346.785", + expectedError: nil, }, - command: []string{"ZDIFF", "ZdiffKey3", "ZdiffKey4", "WITHSCORES"}, - expectedResponse: [][]string{{"one", "1"}, {"two", "2"}}, - expectedError: nil, - }, - { - name: "3. Get the difference between 3 sets with scores.", - presetValues: map[string]interface{}{ - "ZdiffKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + { + name: "4. Increment score to +inf", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "five", Score: 5}, + }), + key: "ZincrbyKey4", + command: []string{"ZINCRBY", "ZincrbyKey4", "+inf", "one"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: sorted_set.Score(math.Inf(1))}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, }), - "ZdiffKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + expectedResponse: "+Inf", + expectedError: nil, + }, + { + name: "5. Increment score to -inf", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, }), - "ZdiffKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, + key: "ZincrbyKey5", + command: []string{"ZINCRBY", "ZincrbyKey5", "-inf", "one"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: sorted_set.Score(math.Inf(-1))}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, }), + expectedResponse: "-Inf", + expectedError: nil, }, - command: []string{"ZDIFF", "ZdiffKey5", "ZdiffKey6", "ZdiffKey7", "WITHSCORES"}, - expectedResponse: [][]string{{"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}}, - expectedError: nil, - }, - { - name: "4. Return sorted set if only one key exists and is a sorted set", - presetValues: map[string]interface{}{ - "ZdiffKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + { + name: "6. Incrementing score by negative increment should lower the score", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "five", Score: 5}, }), - }, - command: []string{"ZDIFF", "ZdiffKey8", "ZdiffKey9", "ZdiffKey10", "WITHSCORES"}, - expectedResponse: [][]string{ - {"one", "1"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, - {"six", "6"}, {"seven", "7"}, {"eight", "8"}, - }, - expectedError: nil, - }, - { - name: "5. Throw error when one of the keys is not a sorted set.", - presetValues: map[string]interface{}{ - "ZdiffKey11": "Default value", - "ZdiffKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + key: "ZincrbyKey6", + command: []string{"ZINCRBY", "ZincrbyKey6", "-2.5", "five"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 2.5}, + }), + expectedResponse: "2.5", + expectedError: nil, + }, + { + name: "7. Return error when attempting to increment on a value that is not a valid sorted set", + presetValue: "Default value", + key: "ZincrbyKey7", + command: []string{"ZINCRBY", "ZincrbyKey7", "-2.5", "five"}, + expectedValue: nil, + expectedResponse: "", + expectedError: errors.New("value at ZincrbyKey7 is not a sorted set"), + }, + { + name: "8. Return error when trying to increment a member that already has score -inf", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: sorted_set.Score(math.Inf(-1))}, + }), + key: "ZincrbyKey8", + command: []string{"ZINCRBY", "ZincrbyKey8", "2.5", "one"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: sorted_set.Score(math.Inf(-1))}, + }), + expectedResponse: "", + expectedError: errors.New("cannot increment -inf or +inf"), + }, + { + name: "9. Return error when trying to increment a member that already has score +inf", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: sorted_set.Score(math.Inf(1))}, + }), + key: "ZincrbyKey9", + command: []string{"ZINCRBY", "ZincrbyKey9", "2.5", "one"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: sorted_set.Score(math.Inf(-1))}, + }), + expectedResponse: "", + expectedError: errors.New("cannot increment -inf or +inf"), + }, + { + name: "10. Return error when increment is not a valid number", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, }), - "ZdiffKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, + key: "ZincrbyKey10", + command: []string{"ZINCRBY", "ZincrbyKey10", "increment", "one"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, }), + expectedResponse: "", + expectedError: errors.New("increment must be a double"), }, - command: []string{"ZDIFF", "ZdiffKey11", "ZdiffKey12", "ZdiffKey13"}, - expectedResponse: nil, - expectedError: errors.New("value at ZdiffKey11 is not a sorted set"), - }, - { - name: "6. Command too short", - command: []string{"ZDIFF"}, - expectedResponse: [][]string{}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + { + name: "11. Command too short", + key: "ZincrbyKey11", + command: []string{"ZINCRBY", "ZincrbyKey11", "one"}, + expectedResponse: "", + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "12. Command too long", + key: "ZincrbyKey12", + command: []string{"ZINCRBY", "ZincrbyKey12", "one", "1", "2"}, + expectedResponse: "", + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { + switch test.presetValue.(type) { case string: command = []resp.Value{ resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), } expected = "ok" case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { command = append(command, []resp.Value{ resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), resp.StringValue(string(member.Value)), }...) } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) } if err = client.WriteArray(command); err != nil { @@ -872,494 +1412,560 @@ func Test_HandleZDIFF(t *testing.T) { } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - return - } - - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) - } - for _, item := range res.Array() { - value := item.Array()[0].String() - score := func() string { - if len(item.Array()) == 2 { - return item.Array()[1].String() + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } - return "" - }() - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - return expected[0] == value - }) { - t.Errorf("unexpected member \"%s\" in response", value) - } - if score != "" { - for _, expected := range test.expectedResponse { - if expected[0] == value && expected[1] != score { - t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) + return + } + + if res.String() != test.expectedResponse { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) + } + + // Check if the resulting sorted set has the expected members/scores + if test.expectedValue == nil { + return + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(test.key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { + t.Error(err) + } + + res, _, err = client.ReadValue() + if err != nil { + t.Error(err) + } + + if len(res.Array()) != test.expectedValue.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + test.key, test.expectedValue.Cardinality(), len(res.Array())) + } + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !test.expectedValue.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) + } + if test.expectedValue.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) + } + } + }) + } + }) + + t.Run("Test_HandleZMPOP", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + preset bool + presetValues map[string]interface{} + command []string + expectedValues map[string]*sorted_set.SortedSet + expectedResponse [][]string + expectedError error + }{ + { + name: "1. Successfully pop one min element by default", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + }, + command: []string{"ZMPOP", "ZmpopKey1"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZmpopKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + }, + expectedResponse: [][]string{ + {"one", "1"}, + }, + expectedError: nil, + }, + { + name: "2. Successfully pop one min element by specifying MIN", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + }, + command: []string{"ZMPOP", "ZmpopKey2", "MIN"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZmpopKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + }, + expectedResponse: [][]string{ + {"one", "1"}, + }, + expectedError: nil, + }, + { + name: "3. Successfully pop one max element by specifying MAX modifier", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + }, + command: []string{"ZMPOP", "ZmpopKey3", "MAX"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZmpopKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + }), + }, + expectedResponse: [][]string{ + {"five", "5"}, + }, + expectedError: nil, + }, + { + name: "4. Successfully pop multiple min elements", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + }), + }, + command: []string{"ZMPOP", "ZmpopKey4", "MIN", "COUNT", "5"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZmpopKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "six", Score: 6}, + }), + }, + expectedResponse: [][]string{ + {"one", "1"}, {"two", "2"}, {"three", "3"}, + {"four", "4"}, {"five", "5"}, + }, + expectedError: nil, + }, + { + name: "5. Successfully pop multiple max elements", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + }), + }, + command: []string{"ZMPOP", "ZmpopKey5", "MAX", "COUNT", "5"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZmpopKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, + }), + }, + expectedResponse: [][]string{{"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}}, + expectedError: nil, + }, + { + name: "6. Successfully pop elements from the first set which is non-empty", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + }), + }, + command: []string{"ZMPOP", "ZmpopKey6", "ZmpopKey7", "MAX", "COUNT", "5"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZmpopKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{}), + "ZmpopKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, + }), + }, + expectedResponse: [][]string{{"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}}, + expectedError: nil, + }, + { + name: "7. Skip the non-set items and pop elements from the first non-empty sorted set found", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopKey8": "Default value", + "ZmpopKey9": "56", + "ZmpopKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + }), + }, + command: []string{"ZMPOP", "ZmpopKey8", "ZmpopKey9", "ZmpopKey10", "ZmpopKey11", "MIN", "COUNT", "5"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZmpopKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{}), + "ZmpopKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "six", Score: 6}, + }), + }, + expectedResponse: [][]string{{"one", "1"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}}, + expectedError: nil, + }, + { + name: "9. Return error when count is a negative integer", + preset: false, + command: []string{"ZMPOP", "ZmpopKey8", "MAX", "COUNT", "-20"}, + expectedError: errors.New("count must be a positive integer"), + }, + { + name: "9. Command too short", + preset: false, + command: []string{"ZMPOP"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } } + } - } - }) - } -} -func Test_HandleZDIFFSTORE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - destination string - command []string - expectedValue *sorted_set.SortedSet - expectedResponse int - expectedError error - }{ - { - name: "1. Get the difference between 2 sorted sets.", - presetValues: map[string]interface{}{ - "ZdiffStoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - "ZdiffStoreKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - destination: "ZdiffStoreDestinationKey1", - command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey1", "ZdiffStoreKey1", "ZdiffStoreKey2"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}, {Value: "two", Score: 2}}), - expectedResponse: 2, - expectedError: nil, - }, - { - name: "2. Get the difference between 3 sorted sets.", - presetValues: map[string]interface{}{ - "ZdiffStoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZdiffStoreKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, - }), - "ZdiffStoreKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZdiffStoreDestinationKey2", - command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey2", "ZdiffStoreKey3", "ZdiffStoreKey4", "ZdiffStoreKey5"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - }), - expectedResponse: 4, - expectedError: nil, - }, - { - name: "3. Return base sorted set element if base set is the only existing key provided and is a valid sorted set", - presetValues: map[string]interface{}{ - "ZdiffStoreKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - destination: "ZdiffStoreDestinationKey3", - command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey3", "ZdiffStoreKey6", "ZdiffStoreKey7", "ZdiffStoreKey8"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - expectedResponse: 8, - expectedError: nil, - }, - { - name: "4. Throw error when base sorted set is not a set.", - presetValues: map[string]interface{}{ - "ZdiffStoreKey9": "Default value", - "ZdiffStoreKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, - }), - "ZdiffStoreKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZdiffStoreDestinationKey4", - command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey4", "ZdiffStoreKey9", "ZdiffStoreKey10", "ZdiffStoreKey11"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: errors.New("value at ZdiffStoreKey9 is not a sorted set"), - }, - { - name: "5. Return 0 when base set is non-existent.", - destination: "ZdiffStoreDestinationKey5", - presetValues: map[string]interface{}{ - "ZdiffStoreKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, - }), - "ZdiffStoreKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey5", "non-existent", "ZdiffStoreKey12", "ZdiffStoreKey13"}, - expectedValue: nil, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "6. Command too short", - command: []string{"ZDIFFSTORE", "ZdiffStoreDestinationKey6"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } + + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) + } + + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) + } + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) + } } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) } + } - if err = client.WriteArray(command); err != nil { + // Check if the resulting sorted set has the expected members/scores + for key, expectedSortedSet := range test.expectedValues { + if expectedSortedSet == nil { + continue + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) + } + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) + } + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) + } } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - - // Check if the resulting sorted set has the expected members/scores - if test.expectedValue == nil { - return - } - - if err = client.WriteArray([]resp.Value{ - resp.StringValue("ZRANGE"), - resp.StringValue(test.destination), - resp.StringValue("-inf"), - resp.StringValue("+inf"), - resp.StringValue("BYSCORE"), - resp.StringValue("WITHSCORES"), - }); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != test.expectedValue.Cardinality() { - t.Errorf("expected resulting set %s to have cardinality %d, got %d", - test.destination, test.expectedValue.Cardinality(), len(res.Array())) - } - - for _, member := range res.Array() { - value := sorted_set.Value(member.Array()[0].String()) - score := sorted_set.Score(member.Array()[1].Float()) - if !test.expectedValue.Contains(value) { - t.Errorf("unexpected value %s in resulting sorted set", value) - } - if test.expectedValue.Get(value).Score != score { - t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) - } - } - }) - } -} + }) + } + }) + + t.Run("Test_HandleZPOP", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + preset bool + presetValues map[string]interface{} + command []string + expectedValues map[string]*sorted_set.SortedSet + expectedResponse [][]string + expectedError error + }{ + { + name: "1. Successfully pop one min element by default", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopMinKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + }, + command: []string{"ZPOPMIN", "ZmpopMinKey1"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZmpopMinKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + }, + expectedResponse: [][]string{ + {"one", "1"}, + }, + expectedError: nil, + }, + { + name: "2. Successfully pop one max element by default", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopMaxKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + }, + command: []string{"ZPOPMAX", "ZmpopMaxKey2"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZmpopMaxKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + }), + }, + expectedResponse: [][]string{ + {"five", "5"}, + }, + expectedError: nil, + }, + { + name: "3. Successfully pop multiple min elements", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopMinKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + }), + }, + command: []string{"ZPOPMIN", "ZmpopMinKey3", "5"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZmpopMinKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "six", Score: 6}, + }), + }, + expectedResponse: [][]string{ + {"one", "1"}, {"two", "2"}, {"three", "3"}, + {"four", "4"}, {"five", "5"}, + }, + expectedError: nil, + }, + { + name: "4. Successfully pop multiple max elements", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopMaxKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + }), + }, + command: []string{"ZPOPMAX", "ZmpopMaxKey4", "5"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZmpopMaxKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, + }), + }, + expectedResponse: [][]string{{"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}}, + expectedError: nil, + }, + { + name: "5. Throw an error when trying to pop from an element that's not a sorted set", + preset: true, + presetValues: map[string]interface{}{ + "ZmpopMinKey5": "Default value", + }, + command: []string{"ZPOPMIN", "ZmpopMinKey5"}, + expectedValues: nil, + expectedResponse: nil, + expectedError: errors.New("value at key ZmpopMinKey5 is not a sorted set"), + }, + { + name: "6. Command too short", + preset: false, + command: []string{"ZPOPMAX"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "7. Command too long", + preset: false, + command: []string{"ZPOPMAX", "ZmpopMaxKey7", "6", "3"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } -func Test_HandleZINCRBY(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValue interface{} - key string - command []string - expectedValue *sorted_set.SortedSet - expectedResponse string - expectedError error - }{ - { - name: "1. Successfully increment by int. Return the new score", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - key: "ZincrbyKey1", - command: []string{"ZINCRBY", "ZincrbyKey1", "5", "one"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 6}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - expectedResponse: "6", - expectedError: nil, - }, - { - name: "2. Successfully increment by float. Return new score", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - key: "ZincrbyKey2", - command: []string{"ZINCRBY", "ZincrbyKey2", "346.785", "one"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 347.785}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - expectedResponse: "347.785", - expectedError: nil, - }, - { - name: "3. Increment on non-existent sorted set will create the set with the member and increment as its score", - presetValue: nil, - key: "ZincrbyKey3", - command: []string{"ZINCRBY", "ZincrbyKey3", "346.785", "one"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 346.785}, - }), - expectedResponse: "346.785", - expectedError: nil, - }, - { - name: "4. Increment score to +inf", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - key: "ZincrbyKey4", - command: []string{"ZINCRBY", "ZincrbyKey4", "+inf", "one"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: sorted_set.Score(math.Inf(1))}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - expectedResponse: "+Inf", - expectedError: nil, - }, - { - name: "5. Increment score to -inf", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - key: "ZincrbyKey5", - command: []string{"ZINCRBY", "ZincrbyKey5", "-inf", "one"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: sorted_set.Score(math.Inf(-1))}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - expectedResponse: "-Inf", - expectedError: nil, - }, - { - name: "6. Incrementing score by negative increment should lower the score", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - key: "ZincrbyKey6", - command: []string{"ZINCRBY", "ZincrbyKey6", "-2.5", "five"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 2.5}, - }), - expectedResponse: "2.5", - expectedError: nil, - }, - { - name: "7. Return error when attempting to increment on a value that is not a valid sorted set", - presetValue: "Default value", - key: "ZincrbyKey7", - command: []string{"ZINCRBY", "ZincrbyKey7", "-2.5", "five"}, - expectedValue: nil, - expectedResponse: "", - expectedError: errors.New("value at ZincrbyKey7 is not a sorted set"), - }, - { - name: "8. Return error when trying to increment a member that already has score -inf", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: sorted_set.Score(math.Inf(-1))}, - }), - key: "ZincrbyKey8", - command: []string{"ZINCRBY", "ZincrbyKey8", "2.5", "one"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: sorted_set.Score(math.Inf(-1))}, - }), - expectedResponse: "", - expectedError: errors.New("cannot increment -inf or +inf"), - }, - { - name: "9. Return error when trying to increment a member that already has score +inf", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: sorted_set.Score(math.Inf(1))}, - }), - key: "ZincrbyKey9", - command: []string{"ZINCRBY", "ZincrbyKey9", "2.5", "one"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: sorted_set.Score(math.Inf(-1))}, - }), - expectedResponse: "", - expectedError: errors.New("cannot increment -inf or +inf"), - }, - { - name: "10. Return error when increment is not a valid number", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, - }), - key: "ZincrbyKey10", - command: []string{"ZINCRBY", "ZincrbyKey10", "increment", "one"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, - }), - expectedResponse: "", - expectedError: errors.New("increment must be a double"), - }, - { - name: "11. Command too short", - key: "ZincrbyKey11", - command: []string{"ZINCRBY", "ZincrbyKey11", "one"}, - expectedResponse: "", - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "12. Command too long", - key: "ZincrbyKey12", - command: []string{"ZINCRBY", "ZincrbyKey12", "one", "1", "2"}, - expectedResponse: "", - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), - } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} - for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -1370,535 +1976,458 @@ func Test_HandleZINCRBY(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } - - if res.String() != test.expectedResponse { - t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) - } - - // Check if the resulting sorted set has the expected members/scores - if test.expectedValue == nil { - return - } - - if err = client.WriteArray([]resp.Value{ - resp.StringValue("ZRANGE"), - resp.StringValue(test.key), - resp.StringValue("-inf"), - resp.StringValue("+inf"), - resp.StringValue("BYSCORE"), - resp.StringValue("WITHSCORES"), - }); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != test.expectedValue.Cardinality() { - t.Errorf("expected resulting set %s to have cardinality %d, got %d", - test.key, test.expectedValue.Cardinality(), len(res.Array())) - } - - for _, member := range res.Array() { - value := sorted_set.Value(member.Array()[0].String()) - score := sorted_set.Score(member.Array()[1].Float()) - if !test.expectedValue.Contains(value) { - t.Errorf("unexpected value %s in resulting sorted set", value) - } - if test.expectedValue.Get(value).Score != score { - t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) - } - } - }) - } -} + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } -func Test_HandleZMPOP(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - preset bool - presetValues map[string]interface{} - command []string - expectedValues map[string]*sorted_set.SortedSet - expectedResponse [][]string - expectedError error - }{ - { - name: "1. Successfully pop one min element by default", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - }, - command: []string{"ZMPOP", "ZmpopKey1"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZmpopKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - }, - expectedResponse: [][]string{ - {"one", "1"}, - }, - expectedError: nil, - }, - { - name: "2. Successfully pop one min element by specifying MIN", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - }, - command: []string{"ZMPOP", "ZmpopKey2", "MIN"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZmpopKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - }, - expectedResponse: [][]string{ - {"one", "1"}, - }, - expectedError: nil, - }, - { - name: "3. Successfully pop one max element by specifying MAX modifier", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - }, - command: []string{"ZMPOP", "ZmpopKey3", "MAX"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZmpopKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - }), - }, - expectedResponse: [][]string{ - {"five", "5"}, - }, - expectedError: nil, - }, - { - name: "4. Successfully pop multiple min elements", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - }), - }, - command: []string{"ZMPOP", "ZmpopKey4", "MIN", "COUNT", "5"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZmpopKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "six", Score: 6}, - }), - }, - expectedResponse: [][]string{ - {"one", "1"}, {"two", "2"}, {"three", "3"}, - {"four", "4"}, {"five", "5"}, - }, - expectedError: nil, - }, - { - name: "5. Successfully pop multiple max elements", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - }), - }, - command: []string{"ZMPOP", "ZmpopKey5", "MAX", "COUNT", "5"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZmpopKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, - }), - }, - expectedResponse: [][]string{{"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}}, - expectedError: nil, - }, - { - name: "6. Successfully pop elements from the first set which is non-empty", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - }), - }, - command: []string{"ZMPOP", "ZmpopKey6", "ZmpopKey7", "MAX", "COUNT", "5"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZmpopKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{}), - "ZmpopKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, - }), - }, - expectedResponse: [][]string{{"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}}, - expectedError: nil, - }, - { - name: "7. Skip the non-set items and pop elements from the first non-empty sorted set found", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopKey8": "Default value", - "ZmpopKey9": "56", - "ZmpopKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - }), - }, - command: []string{"ZMPOP", "ZmpopKey8", "ZmpopKey9", "ZmpopKey10", "ZmpopKey11", "MIN", "COUNT", "5"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZmpopKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{}), - "ZmpopKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "six", Score: 6}, - }), - }, - expectedResponse: [][]string{{"one", "1"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}}, - expectedError: nil, - }, - { - name: "9. Return error when count is a negative integer", - preset: false, - command: []string{"ZMPOP", "ZmpopKey8", "MAX", "COUNT", "-20"}, - expectedError: errors.New("count must be a positive integer"), - }, - { - name: "9. Command too short", - preset: false, - command: []string{"ZMPOP"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) + } + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) + } } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) } + } - if err = client.WriteArray(command); err != nil { + // Check if the resulting sorted set has the expected members/scores + for key, expectedSortedSet := range test.expectedValues { + if expectedSortedSet == nil { + continue + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) + } + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) + } + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) + } } } + }) + } + }) + + t.Run("Test_HandleZMSCORE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedResponse []string + expectedError error + }{ + { + // 1. Return multiple scores from the sorted set. + // Return nil for elements that do not exist in the sorted set. + name: "1. Return multiple scores from the sorted set.", + presetValues: map[string]interface{}{ + "ZmScoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, + {Value: "three", Score: 3}, {Value: "four", Score: 4.055}, + {Value: "five", Score: 5}, + }), + }, + command: []string{"ZMSCORE", "ZmScoreKey1", "one", "none", "two", "one", "three", "four", "none", "five"}, + expectedResponse: []string{"1.1", "", "245", "1.1", "3", "4.055", "", "5"}, + expectedError: nil, + }, + { + name: "2. If key does not exist, return empty array", + presetValues: nil, + command: []string{"ZMSCORE", "ZmScoreKey2", "one", "two", "three", "four"}, + expectedResponse: []string{}, + expectedError: nil, + }, + { + name: "3. Throw error when trying to find scores from elements that are not sorted sets", + presetValues: map[string]interface{}{"ZmScoreKey3": "Default value"}, + command: []string{"ZMSCORE", "ZmScoreKey3", "one", "two", "three"}, + expectedError: errors.New("value at ZmScoreKey3 is not a sorted set"), + }, + { + name: "9. Command too short", + command: []string{"ZMSCORE"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - return - } - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - for _, item := range res.Array() { - value := item.Array()[0].String() - score := func() string { - if len(item.Array()) == 2 { - return item.Array()[1].String() - } - return "" - }() - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - return expected[0] == value - }) { - t.Errorf("unexpected member \"%s\" in response", value) - } - if score != "" { - for _, expected := range test.expectedResponse { - if expected[0] == value && expected[1] != score { - t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) - } + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } + return } - } - // Check if the resulting sorted set has the expected members/scores - for key, expectedSortedSet := range test.expectedValues { - if expectedSortedSet == nil { - continue + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) } - if err = client.WriteArray([]resp.Value{ - resp.StringValue("ZRANGE"), - resp.StringValue(key), - resp.StringValue("-inf"), - resp.StringValue("+inf"), - resp.StringValue("BYSCORE"), - resp.StringValue("WITHSCORES"), - }); err != nil { - t.Error(err) + for i := 0; i < len(res.Array()); i++ { + if test.expectedResponse[i] != res.Array()[i].String() { + t.Errorf("expected element at index %d to be \"%s\", got %s", + i, test.expectedResponse[i], res.Array()[i].String()) + } } + }) + } + }) + + t.Run("Test_HandleZSCORE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedResponse string + expectedError error + }{ + { + name: "1. Return score from a sorted set.", + presetValues: map[string]interface{}{ + "ZscoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, + {Value: "three", Score: 3}, {Value: "four", Score: 4.055}, + {Value: "five", Score: 5}, + }), + }, + command: []string{"ZSCORE", "ZscoreKey1", "four"}, + expectedResponse: "4.055", + expectedError: nil, + }, + { + name: "2. If key does not exist, return nil value", + presetValues: nil, + command: []string{"ZSCORE", "ZscoreKey2", "one"}, + expectedResponse: "", + expectedError: nil, + }, + { + name: "3. If key exists and is a sorted set, but the member does not exist, return nil", + presetValues: map[string]interface{}{ + "ZscoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, + {Value: "three", Score: 3}, {Value: "four", Score: 4.055}, + {Value: "five", Score: 5}, + }), + }, + command: []string{"ZSCORE", "ZscoreKey3", "non-existent"}, + expectedResponse: "", + expectedError: nil, + }, + { + name: "4. Throw error when trying to find scores from elements that are not sorted sets", + presetValues: map[string]interface{}{"ZscoreKey4": "Default value"}, + command: []string{"ZSCORE", "ZscoreKey4", "one"}, + expectedError: errors.New("value at ZscoreKey4 is not a sorted set"), + }, + { + name: "5. Command too short", + command: []string{"ZSCORE"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + command: []string{"ZSCORE", "ZscoreKey5", "one", "two"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) } - if len(res.Array()) != expectedSortedSet.Cardinality() { - t.Errorf("expected resulting set %s to have cardinality %d, got %d", - key, expectedSortedSet.Cardinality(), len(res.Array())) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - for _, member := range res.Array() { - value := sorted_set.Value(member.Array()[0].String()) - score := sorted_set.Score(member.Array()[1].Float()) - if !expectedSortedSet.Contains(value) { - t.Errorf("unexpected value %s in resulting sorted set", value) - } - if expectedSortedSet.Get(value).Score != score { - t.Errorf("expected value %s to have score %v, got %v", - value, expectedSortedSet.Get(value).Score, score) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - } - }) - } -} -func Test_HandleZPOP(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - preset bool - presetValues map[string]interface{} - command []string - expectedValues map[string]*sorted_set.SortedSet - expectedResponse [][]string - expectedError error - }{ - { - name: "1. Successfully pop one min element by default", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopMinKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - }, - command: []string{"ZPOPMIN", "ZmpopMinKey1"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZmpopMinKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - }, - expectedResponse: [][]string{ - {"one", "1"}, - }, - expectedError: nil, - }, - { - name: "2. Successfully pop one max element by default", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopMaxKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - }, - command: []string{"ZPOPMAX", "ZmpopMaxKey2"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZmpopMaxKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - }), - }, - expectedResponse: [][]string{ - {"five", "5"}, - }, - expectedError: nil, - }, - { - name: "3. Successfully pop multiple min elements", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopMinKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - }), - }, - command: []string{"ZPOPMIN", "ZmpopMinKey3", "5"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZmpopMinKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "six", Score: 6}, - }), - }, - expectedResponse: [][]string{ - {"one", "1"}, {"two", "2"}, {"three", "3"}, - {"four", "4"}, {"five", "5"}, - }, - expectedError: nil, - }, - { - name: "4. Successfully pop multiple max elements", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopMaxKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - }), - }, - command: []string{"ZPOPMAX", "ZmpopMaxKey4", "5"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZmpopMaxKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, - }), - }, - expectedResponse: [][]string{{"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}}, - expectedError: nil, - }, - { - name: "5. Throw an error when trying to pop from an element that's not a sorted set", - preset: true, - presetValues: map[string]interface{}{ - "ZmpopMinKey5": "Default value", - }, - command: []string{"ZPOPMIN", "ZmpopMinKey5"}, - expectedValues: nil, - expectedResponse: nil, - expectedError: errors.New("value at key ZmpopMinKey5 is not a sorted set"), - }, - { - name: "6. Command too short", - preset: false, - command: []string{"ZPOPMAX"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "7. Command too long", - preset: false, - command: []string{"ZPOPMAX", "ZmpopMaxKey7", "6", "3"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { + if res.String() != test.expectedResponse { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) + } + }) + } + }) + + t.Run("Test_HandleZRANDMEMBER", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + key string + presetValue interface{} + command []string + expectedValue int // The final cardinality of the resulting set + allowRepeat bool + expectedResponse [][]string + expectedError error + }{ + { + // 1. Return multiple random elements without removing them. + // Count is positive, do not allow repeated elements + name: "1. Return multiple random elements without removing them.", + key: "ZrandMemberKey1", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + command: []string{"ZRANDMEMBER", "ZrandMemberKey1", "3"}, + expectedValue: 8, + allowRepeat: false, + expectedResponse: [][]string{ + {"one"}, {"two"}, {"three"}, {"four"}, + {"five"}, {"six"}, {"seven"}, {"eight"}, + }, + expectedError: nil, + }, + { + // 2. Return multiple random elements and their scores without removing them. + // Count is negative, so allow repeated numbers. + name: "2. Return multiple random elements and their scores without removing them.", + key: "ZrandMemberKey2", + presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + command: []string{"ZRANDMEMBER", "ZrandMemberKey2", "-5", "WITHSCORES"}, + expectedValue: 8, + allowRepeat: true, + expectedResponse: [][]string{ + {"one", "1"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, + {"five", "5"}, {"six", "6"}, {"seven", "7"}, {"eight", "8"}, + }, + expectedError: nil, + }, + { + name: "2. Return error when the source key is not a sorted set.", + key: "ZrandMemberKey3", + presetValue: "Default value", + command: []string{"ZRANDMEMBER", "ZrandMemberKey3"}, + expectedValue: 0, + expectedError: errors.New("value at ZrandMemberKey3 is not a sorted set"), + }, + { + name: "5. Command too short", + command: []string{"ZRANDMEMBER"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + command: []string{"ZRANDMEMBER", "source5", "source6", "member1", "member2"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "7. Throw error when count is not an integer", + command: []string{"ZRANDMEMBER", "ZrandMemberKey1", "count"}, + expectedError: errors.New("count must be an integer"), + }, + { + name: "8. Throw error when the fourth argument is not WITHSCORES", + command: []string{"ZRANDMEMBER", "ZrandMemberKey1", "8", "ANOTHER"}, + expectedError: errors.New("last option must be WITHSCORES"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != nil { + var command []resp.Value + var expected string + + switch test.presetValue.(type) { case string: command = []resp.Value{ resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + resp.StringValue(test.key), + resp.StringValue(test.presetValue.(string)), } expected = "ok" case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} + for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { command = append(command, []resp.Value{ resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), resp.StringValue(string(member.Value)), }...) } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) } if err = client.WriteArray(command); err != nil { @@ -1914,458 +2443,749 @@ func Test_HandleZPOP(t *testing.T) { } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - return - } - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) - } + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return + } - for _, item := range res.Array() { - value := item.Array()[0].String() - score := func() string { - if len(item.Array()) == 2 { - return item.Array()[1].String() + // Check that each of the returned elements is in the expected response. + for _, item := range res.Array() { + value := sorted_set.Value(item.Array()[0].String()) + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == string(value) + }) { + t.Errorf("unexected element \"%s\" in response", value) } - return "" - }() - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - return expected[0] == value - }) { - t.Errorf("unexpected member \"%s\" in response", value) - } - if score != "" { for _, expected := range test.expectedResponse { - if expected[0] == value && expected[1] != score { - t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) + if len(item.Array()) != len(expected) { + t.Errorf("expected response for element \"%s\" to have length %d, got %d", + value, len(expected), len(item.Array())) + } + if expected[0] != string(value) { + continue + } + if len(expected) == 2 { + score := item.Array()[1].String() + if expected[1] != score { + t.Errorf("expected score for memebr \"%s\" to be %s, got %s", value, expected[1], score) + } } } } - } - // Check if the resulting sorted set has the expected members/scores - for key, expectedSortedSet := range test.expectedValues { - if expectedSortedSet == nil { - continue + // Check that allowRepeat determines whether elements are repeated or not. + if !test.allowRepeat { + ss := sorted_set.NewSortedSet([]sorted_set.MemberParam{}) + for _, item := range res.Array() { + member := sorted_set.Value(item.Array()[0].String()) + score := func() sorted_set.Score { + if len(item.Array()) == 2 { + return sorted_set.Score(item.Array()[1].Float()) + } + return sorted_set.Score(0) + }() + _, err = ss.AddOrUpdate( + []sorted_set.MemberParam{{member, score}}, + nil, nil, nil, nil) + if err != nil { + t.Error(err) + } + } + if len(res.Array()) != ss.Cardinality() { + t.Error("unexpected repeated elements in response") + } } + }) + } + }) + + t.Run("Test_HandleZRANK", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedResponse []string + expectedError error + }{ + { + name: "1. Return element's rank from a sorted set.", + presetValues: map[string]interface{}{ + "ZrankKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + }, + command: []string{"ZRANK", "ZrankKey1", "four"}, + expectedResponse: []string{"3"}, + expectedError: nil, + }, + { + name: "2. Return element's rank from a sorted set with its score.", + presetValues: map[string]interface{}{ + "ZrankKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100.1}, {Value: "two", Score: 245}, + {Value: "three", Score: 305.43}, {Value: "four", Score: 411.055}, + {Value: "five", Score: 500}, + }), + }, + command: []string{"ZRANK", "ZrankKey1", "four", "WITHSCORES"}, + expectedResponse: []string{"3", "411.055"}, + expectedError: nil, + }, + { + name: "3. If key does not exist, return nil value", + presetValues: nil, + command: []string{"ZRANK", "ZrankKey3", "one"}, + expectedResponse: nil, + expectedError: nil, + }, + { + name: "4. If key exists and is a sorted set, but the member does not exist, return nil", + presetValues: map[string]interface{}{ + "ZrankKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, + {Value: "three", Score: 3}, {Value: "four", Score: 4.055}, + {Value: "five", Score: 5}, + }), + }, + command: []string{"ZRANK", "ZrankKey4", "non-existent"}, + expectedResponse: nil, + expectedError: nil, + }, + { + name: "5. Throw error when trying to find scores from elements that are not sorted sets", + presetValues: map[string]interface{}{"ZrankKey5": "Default value"}, + command: []string{"ZRANK", "ZrankKey5", "one"}, + expectedError: errors.New("value at ZrankKey5 is not a sorted set"), + }, + { + name: "5. Command too short", + command: []string{"ZRANK"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + command: []string{"ZRANK", "ZrankKey5", "one", "WITHSCORES", "two"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } - if err = client.WriteArray([]resp.Value{ - resp.StringValue("ZRANGE"), - resp.StringValue(key), - resp.StringValue("-inf"), - resp.StringValue("+inf"), - resp.StringValue("BYSCORE"), - resp.StringValue("WITHSCORES"), - }); err != nil { - t.Error(err) } - res, _, err = client.ReadValue() + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if len(res.Array()) != expectedSortedSet.Cardinality() { - t.Errorf("expected resulting set %s to have cardinality %d, got %d", - key, expectedSortedSet.Cardinality(), len(res.Array())) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - for _, member := range res.Array() { - value := sorted_set.Value(member.Array()[0].String()) - score := sorted_set.Score(member.Array()[1].Float()) - if !expectedSortedSet.Contains(value) { - t.Errorf("unexpected value %s in resulting sorted set", value) - } - if expectedSortedSet.Get(value).Score != score { - t.Errorf("expected value %s to have score %v, got %v", - value, expectedSortedSet.Get(value).Score, score) - } + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) } - } - }) - } -} -func Test_HandleZMSCORE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedResponse []string - expectedError error - }{ - { - // 1. Return multiple scores from the sorted set. - // Return nil for elements that do not exist in the sorted set. - name: "1. Return multiple scores from the sorted set.", - presetValues: map[string]interface{}{ - "ZmScoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, - {Value: "three", Score: 3}, {Value: "four", Score: 4.055}, - {Value: "five", Score: 5}, - }), - }, - command: []string{"ZMSCORE", "ZmScoreKey1", "one", "none", "two", "one", "three", "four", "none", "five"}, - expectedResponse: []string{"1.1", "", "245", "1.1", "3", "4.055", "", "5"}, - expectedError: nil, - }, - { - name: "2. If key does not exist, return empty array", - presetValues: nil, - command: []string{"ZMSCORE", "ZmScoreKey2", "one", "two", "three", "four"}, - expectedResponse: []string{}, - expectedError: nil, - }, - { - name: "3. Throw error when trying to find scores from elements that are not sorted sets", - presetValues: map[string]interface{}{"ZmScoreKey3": "Default value"}, - command: []string{"ZMSCORE", "ZmScoreKey3", "one", "two", "three"}, - expectedError: errors.New("value at ZmScoreKey3 is not a sorted set"), - }, - { - name: "9. Command too short", - command: []string{"ZMSCORE"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + for i := 0; i < len(res.Array()); i++ { + if test.expectedResponse[i] != res.Array()[i].String() { + t.Errorf("expected element at index %d to be \"%s\", got %s", + i, test.expectedResponse[i], res.Array()[i].String()) + } + } + }) + } + }) + + t.Run("Test_HandleZREM", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedValues map[string]*sorted_set.SortedSet + expectedResponse int + expectedError error + }{ + { + // Successfully remove multiple elements from sorted set, skipping non-existent members. + // Return deleted count. + name: "1. Successfully remove multiple elements from sorted set, skipping non-existent members.", + presetValues: map[string]interface{}{ + "ZremKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + }), + }, + command: []string{"ZREM", "ZremKey1", "three", "four", "five", "none", "six", "none", "seven"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZremKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + }), + }, + expectedResponse: 5, + expectedError: nil, + }, + { + name: "2. If key does not exist, return 0", + presetValues: nil, + command: []string{"ZREM", "ZremKey2", "member"}, + expectedValues: nil, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "3. Return error key is not a sorted set", + presetValues: map[string]interface{}{ + "ZremKey3": "Default value", + }, + command: []string{"ZREM", "ZremKey3", "member"}, + expectedError: errors.New("value at ZremKey3 is not a sorted set"), + }, + { + name: "9. Command too short", + command: []string{"ZREM"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) } - if err = client.WriteArray(command); err != nil { + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) + } + + // Check if the resulting sorted set has the expected members/scores + for key, expectedSortedSet := range test.expectedValues { + if expectedSortedSet == nil { + continue + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) + } + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) + } + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) + } } } + }) + } + }) + + t.Run("Test_HandleZREMRANGEBYSCORE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedValues map[string]*sorted_set.SortedSet + expectedResponse int + expectedError error + }{ + { + name: "1. Successfully remove multiple elements with scores inside the provided range", + presetValues: map[string]interface{}{ + "ZremRangeByScoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + }), + }, + command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey1", "3", "7"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZremRangeByScoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + }), + }, + expectedResponse: 5, + expectedError: nil, + }, + { + name: "2. If key does not exist, return 0", + presetValues: nil, + command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey2", "2", "4"}, + expectedValues: nil, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "3. Return error key is not a sorted set", + presetValues: map[string]interface{}{ + "ZremRangeByScoreKey3": "Default value", + }, + command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey3", "4", "4"}, + expectedError: errors.New("value at ZremRangeByScoreKey3 is not a sorted set"), + }, + { + name: "4. Command too short", + command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey4", "3"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "5. Command too long", + command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey5", "4", "5", "8"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - return - } - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - for i := 0; i < len(res.Array()); i++ { - if test.expectedResponse[i] != res.Array()[i].String() { - t.Errorf("expected element at index %d to be \"%s\", got %s", - i, test.expectedResponse[i], res.Array()[i].String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - } - }) - } -} -func Test_HandleZSCORE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedResponse string - expectedError error - }{ - { - name: "1. Return score from a sorted set.", - presetValues: map[string]interface{}{ - "ZscoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, - {Value: "three", Score: 3}, {Value: "four", Score: 4.055}, - {Value: "five", Score: 5}, - }), - }, - command: []string{"ZSCORE", "ZscoreKey1", "four"}, - expectedResponse: "4.055", - expectedError: nil, - }, - { - name: "2. If key does not exist, return nil value", - presetValues: nil, - command: []string{"ZSCORE", "ZscoreKey2", "one"}, - expectedResponse: "", - expectedError: nil, - }, - { - name: "3. If key exists and is a sorted set, but the member does not exist, return nil", - presetValues: map[string]interface{}{ - "ZscoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, - {Value: "three", Score: 3}, {Value: "four", Score: 4.055}, - {Value: "five", Score: 5}, - }), - }, - command: []string{"ZSCORE", "ZscoreKey3", "non-existent"}, - expectedResponse: "", - expectedError: nil, - }, - { - name: "4. Throw error when trying to find scores from elements that are not sorted sets", - presetValues: map[string]interface{}{"ZscoreKey4": "Default value"}, - command: []string{"ZSCORE", "ZscoreKey4", "one"}, - expectedError: errors.New("value at ZscoreKey4 is not a sorted set"), - }, - { - name: "5. Command too short", - command: []string{"ZSCORE"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - command: []string{"ZSCORE", "ZscoreKey5", "one", "two"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), - } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) - } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + // Check if the resulting sorted set has the expected members/scores + for key, expectedSortedSet := range test.expectedValues { + if expectedSortedSet == nil { + continue } - if err = client.WriteArray(command); err != nil { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) } - } - } + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) + } + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) + } + } + } + }) + } + }) + + t.Run("Test_HandleZREMRANGEBYRANK", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedValues map[string]*sorted_set.SortedSet + expectedResponse int + expectedError error + }{ + { + name: "1. Successfully remove multiple elements within range", + presetValues: map[string]interface{}{ + "ZremRangeByRankKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + }), + }, + command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey1", "0", "5"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZremRangeByRankKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + }), + }, + expectedResponse: 6, + expectedError: nil, + }, + { + name: "2. Establish boundaries from the end of the set when negative boundaries are provided", + presetValues: map[string]interface{}{ + "ZremRangeByRankKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + }), + }, + command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey2", "-6", "-3"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZremRangeByRankKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + }), + }, + expectedResponse: 4, + expectedError: nil, + }, + { + name: "3. If key does not exist, return 0", + presetValues: nil, + command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey3", "2", "4"}, + expectedValues: nil, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "4. Return error key is not a sorted set", + presetValues: map[string]interface{}{ + "ZremRangeByRankKey3": "Default value", + }, + command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey3", "4", "4"}, + expectedError: errors.New("value at ZremRangeByRankKey3 is not a sorted set"), + }, + { + name: "5. Return error when start index is out of bounds", + presetValues: map[string]interface{}{ + "ZremRangeByRankKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + }), + }, + command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey5", "-12", "5"}, + expectedValues: nil, + expectedResponse: 0, + expectedError: errors.New("indices out of bounds"), + }, + { + name: "6. Return error when end index is out of bounds", + presetValues: map[string]interface{}{ + "ZremRangeByRankKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + }), + }, + command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey6", "0", "11"}, + expectedValues: nil, + expectedResponse: 0, + expectedError: errors.New("indices out of bounds"), + }, + { + name: "7. Command too short", + command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey4", "3"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "8. Command too long", + command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey7", "4", "5", "8"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } - return - } - if res.String() != test.expectedResponse { - t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) - } - }) - } -} - -func Test_HandleZRANDMEMBER(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - key string - presetValue interface{} - command []string - expectedValue int // The final cardinality of the resulting set - allowRepeat bool - expectedResponse [][]string - expectedError error - }{ - { - // 1. Return multiple random elements without removing them. - // Count is positive, do not allow repeated elements - name: "1. Return multiple random elements without removing them.", - key: "ZrandMemberKey1", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - command: []string{"ZRANDMEMBER", "ZrandMemberKey1", "3"}, - expectedValue: 8, - allowRepeat: false, - expectedResponse: [][]string{ - {"one"}, {"two"}, {"three"}, {"four"}, - {"five"}, {"six"}, {"seven"}, {"eight"}, - }, - expectedError: nil, - }, - { - // 2. Return multiple random elements and their scores without removing them. - // Count is negative, so allow repeated numbers. - name: "2. Return multiple random elements and their scores without removing them.", - key: "ZrandMemberKey2", - presetValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - command: []string{"ZRANDMEMBER", "ZrandMemberKey2", "-5", "WITHSCORES"}, - expectedValue: 8, - allowRepeat: true, - expectedResponse: [][]string{ - {"one", "1"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, - {"five", "5"}, {"six", "6"}, {"seven", "7"}, {"eight", "8"}, - }, - expectedError: nil, - }, - { - name: "2. Return error when the source key is not a sorted set.", - key: "ZrandMemberKey3", - presetValue: "Default value", - command: []string{"ZRANDMEMBER", "ZrandMemberKey3"}, - expectedValue: 0, - expectedError: errors.New("value at ZrandMemberKey3 is not a sorted set"), - }, - { - name: "5. Command too short", - command: []string{"ZRANDMEMBER"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - command: []string{"ZRANDMEMBER", "source5", "source6", "member1", "member2"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "7. Throw error when count is not an integer", - command: []string{"ZRANDMEMBER", "ZrandMemberKey1", "count"}, - expectedError: errors.New("count must be an integer"), - }, - { - name: "8. Throw error when the fourth argument is not WITHSCORES", - command: []string{"ZRANDMEMBER", "ZrandMemberKey1", "8", "ANOTHER"}, - expectedError: errors.New("last option must be WITHSCORES"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != nil { - var command []resp.Value - var expected string - - switch test.presetValue.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue.(string)), - } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(test.key)} - for _, member := range test.presetValue.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) - } - expected = strconv.Itoa(test.presetValue.(*sorted_set.SortedSet).Cardinality()) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } if err = client.WriteArray(command); err != nil { @@ -2376,368 +3196,881 @@ func Test_HandleZRANDMEMBER(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if res.Integer() != test.expectedResponse { + t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) } - return - } - // Check that each of the returned elements is in the expected response. - for _, item := range res.Array() { - value := sorted_set.Value(item.Array()[0].String()) - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - return expected[0] == string(value) - }) { - t.Errorf("unexected element \"%s\" in response", value) - } - for _, expected := range test.expectedResponse { - if len(item.Array()) != len(expected) { - t.Errorf("expected response for element \"%s\" to have length %d, got %d", - value, len(expected), len(item.Array())) - } - if expected[0] != string(value) { + // Check if the resulting sorted set has the expected members/scores + for key, expectedSortedSet := range test.expectedValues { + if expectedSortedSet == nil { continue } - if len(expected) == 2 { - score := item.Array()[1].String() - if expected[1] != score { - t.Errorf("expected score for memebr \"%s\" to be %s, got %s", value, expected[1], score) - } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { + t.Error(err) } - } - } - // Check that allowRepeat determines whether elements are repeated or not. - if !test.allowRepeat { - ss := sorted_set.NewSortedSet([]sorted_set.MemberParam{}) - for _, item := range res.Array() { - member := sorted_set.Value(item.Array()[0].String()) - score := func() sorted_set.Score { - if len(item.Array()) == 2 { - return sorted_set.Score(item.Array()[1].Float()) - } - return sorted_set.Score(0) - }() - _, err = ss.AddOrUpdate( - []sorted_set.MemberParam{{member, score}}, - nil, nil, nil, nil) + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - } - if len(res.Array()) != ss.Cardinality() { - t.Error("unexpected repeated elements in response") - } - } - }) - } -} -func Test_HandleZRANK(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedResponse []string - expectedError error - }{ - { - name: "1. Return element's rank from a sorted set.", - presetValues: map[string]interface{}{ - "ZrankKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - }, - command: []string{"ZRANK", "ZrankKey1", "four"}, - expectedResponse: []string{"3"}, - expectedError: nil, - }, - { - name: "2. Return element's rank from a sorted set with its score.", - presetValues: map[string]interface{}{ - "ZrankKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100.1}, {Value: "two", Score: 245}, - {Value: "three", Score: 305.43}, {Value: "four", Score: 411.055}, - {Value: "five", Score: 500}, - }), - }, - command: []string{"ZRANK", "ZrankKey1", "four", "WITHSCORES"}, - expectedResponse: []string{"3", "411.055"}, - expectedError: nil, - }, - { - name: "3. If key does not exist, return nil value", - presetValues: nil, - command: []string{"ZRANK", "ZrankKey3", "one"}, - expectedResponse: nil, - expectedError: nil, - }, - { - name: "4. If key exists and is a sorted set, but the member does not exist, return nil", - presetValues: map[string]interface{}{ - "ZrankKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1.1}, {Value: "two", Score: 245}, - {Value: "three", Score: 3}, {Value: "four", Score: 4.055}, - {Value: "five", Score: 5}, - }), - }, - command: []string{"ZRANK", "ZrankKey4", "non-existent"}, - expectedResponse: nil, - expectedError: nil, - }, - { - name: "5. Throw error when trying to find scores from elements that are not sorted sets", - presetValues: map[string]interface{}{"ZrankKey5": "Default value"}, - command: []string{"ZRANK", "ZrankKey5", "one"}, - expectedError: errors.New("value at ZrankKey5 is not a sorted set"), - }, - { - name: "5. Command too short", - command: []string{"ZRANK"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - command: []string{"ZRANK", "ZrankKey5", "one", "WITHSCORES", "two"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) + } + } + } + }) + } + }) + + t.Run("Test_HandleZREMRANGEBYLEX", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedValues map[string]*sorted_set.SortedSet + expectedResponse int + expectedError error + }{ + { + name: "1. Successfully remove multiple elements with scores inside the provided range", + presetValues: map[string]interface{}{ + "ZremRangeByLexKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "b", Score: 1}, + {Value: "c", Score: 1}, {Value: "d", Score: 1}, + {Value: "e", Score: 1}, {Value: "f", Score: 1}, + {Value: "g", Score: 1}, {Value: "h", Score: 1}, + {Value: "i", Score: 1}, {Value: "j", Score: 1}, + }), + }, + command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey1", "a", "d"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZremRangeByLexKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "e", Score: 1}, {Value: "f", Score: 1}, + {Value: "g", Score: 1}, {Value: "h", Score: 1}, + {Value: "i", Score: 1}, {Value: "j", Score: 1}, + }), + }, + expectedResponse: 4, + expectedError: nil, + }, + { + name: "2. Return 0 if the members do not have the same score", + presetValues: map[string]interface{}{ + "ZremRangeByLexKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "b", Score: 2}, + {Value: "c", Score: 3}, {Value: "d", Score: 4}, + {Value: "e", Score: 5}, {Value: "f", Score: 6}, + {Value: "g", Score: 7}, {Value: "h", Score: 8}, + {Value: "i", Score: 9}, {Value: "j", Score: 10}, + }), + }, + command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey2", "d", "g"}, + expectedValues: map[string]*sorted_set.SortedSet{ + "ZremRangeByLexKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "b", Score: 2}, + {Value: "c", Score: 3}, {Value: "d", Score: 4}, + {Value: "e", Score: 5}, {Value: "f", Score: 6}, + {Value: "g", Score: 7}, {Value: "h", Score: 8}, + {Value: "i", Score: 9}, {Value: "j", Score: 10}, + }), + }, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "3. If key does not exist, return 0", + presetValues: nil, + command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey3", "2", "4"}, + expectedValues: nil, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "4. Return error key is not a sorted set", + presetValues: map[string]interface{}{ + "ZremRangeByLexKey3": "Default value", + }, + command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey3", "a", "d"}, + expectedError: errors.New("value at ZremRangeByLexKey3 is not a sorted set"), + }, + { + name: "5. Command too short", + command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey4", "a"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "6. Command too long", + command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey5", "a", "b", "c"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) } - if err = client.WriteArray(command); err != nil { + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return + } + + if res.Integer() != test.expectedResponse { + t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) + } + + // Check if the resulting sorted set has the expected members/scores + for key, expectedSortedSet := range test.expectedValues { + if expectedSortedSet == nil { + continue + } + + if err = client.WriteArray([]resp.Value{ + resp.StringValue("ZRANGE"), + resp.StringValue(key), + resp.StringValue("-inf"), + resp.StringValue("+inf"), + resp.StringValue("BYSCORE"), + resp.StringValue("WITHSCORES"), + }); err != nil { t.Error(err) } - res, _, err := client.ReadValue() + + res, _, err = client.ReadValue() if err != nil { t.Error(err) } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if len(res.Array()) != expectedSortedSet.Cardinality() { + t.Errorf("expected resulting set %s to have cardinality %d, got %d", + key, expectedSortedSet.Cardinality(), len(res.Array())) + } + + for _, member := range res.Array() { + value := sorted_set.Value(member.Array()[0].String()) + score := sorted_set.Score(member.Array()[1].Float()) + if !expectedSortedSet.Contains(value) { + t.Errorf("unexpected value %s in resulting sorted set", value) + } + if expectedSortedSet.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", + value, expectedSortedSet.Get(value).Score, score) + } } } + }) + } + }) + + t.Run("Test_HandleZRANGE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedResponse [][]string + expectedError error + }{ + { + name: "1. Get elements withing score range without score.", + presetValues: map[string]interface{}{ + "ZrangeKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + command: []string{"ZRANGE", "ZrangeKey1", "3", "7", "BYSCORE"}, + expectedResponse: [][]string{{"three"}, {"four"}, {"five"}, {"six"}, {"seven"}}, + expectedError: nil, + }, + { + name: "2. Get elements within score range with score.", + presetValues: map[string]interface{}{ + "ZrangeKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + command: []string{"ZRANGE", "ZrangeKey2", "3", "7", "BYSCORE", "WITHSCORES"}, + expectedResponse: [][]string{ + {"three", "3"}, {"four", "4"}, {"five", "5"}, + {"six", "6"}, {"seven", "7"}}, + expectedError: nil, + }, + { + // 3. Get elements within score range with offset and limit. + // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). + name: "3. Get elements within score range with offset and limit.", + presetValues: map[string]interface{}{ + "ZrangeKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + command: []string{"ZRANGE", "ZrangeKey3", "3", "7", "BYSCORE", "WITHSCORES", "LIMIT", "2", "4"}, + expectedResponse: [][]string{{"three", "3"}, {"four", "4"}, {"five", "5"}}, + expectedError: nil, + }, + { + // 4. Get elements within score range with offset and limit + reverse the results. + // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). + // REV reverses the original set before getting the range. + name: "4. Get elements within score range with offset and limit + reverse the results.", + presetValues: map[string]interface{}{ + "ZrangeKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + command: []string{"ZRANGE", "ZrangeKey4", "3", "7", "BYSCORE", "WITHSCORES", "LIMIT", "2", "4", "REV"}, + expectedResponse: [][]string{{"six", "6"}, {"five", "5"}, {"four", "4"}}, + expectedError: nil, + }, + { + name: "5. Get elements within lex range without score.", + presetValues: map[string]interface{}{ + "ZrangeKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "e", Score: 1}, + {Value: "b", Score: 1}, {Value: "f", Score: 1}, + {Value: "c", Score: 1}, {Value: "g", Score: 1}, + {Value: "d", Score: 1}, {Value: "h", Score: 1}, + }), + }, + command: []string{"ZRANGE", "ZrangeKey5", "c", "g", "BYLEX"}, + expectedResponse: [][]string{{"c"}, {"d"}, {"e"}, {"f"}, {"g"}}, + expectedError: nil, + }, + { + name: "6. Get elements within lex range with score.", + presetValues: map[string]interface{}{ + "ZrangeKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "e", Score: 1}, + {Value: "b", Score: 1}, {Value: "f", Score: 1}, + {Value: "c", Score: 1}, {Value: "g", Score: 1}, + {Value: "d", Score: 1}, {Value: "h", Score: 1}, + }), + }, + command: []string{"ZRANGE", "ZrangeKey6", "a", "f", "BYLEX", "WITHSCORES"}, + expectedResponse: [][]string{ + {"a", "1"}, {"b", "1"}, {"c", "1"}, + {"d", "1"}, {"e", "1"}, {"f", "1"}}, + expectedError: nil, + }, + { + // 7. Get elements within lex range with offset and limit. + // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). + name: "7. Get elements within lex range with offset and limit.", + presetValues: map[string]interface{}{ + "ZrangeKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "b", Score: 1}, + {Value: "c", Score: 1}, {Value: "d", Score: 1}, + {Value: "e", Score: 1}, {Value: "f", Score: 1}, + {Value: "g", Score: 1}, {Value: "h", Score: 1}, + }), + }, + command: []string{"ZRANGE", "ZrangeKey7", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, + expectedResponse: [][]string{{"c", "1"}, {"d", "1"}, {"e", "1"}}, + expectedError: nil, + }, + { + // 8. Get elements within lex range with offset and limit + reverse the results. + // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). + // REV reverses the original set before getting the range. + name: "8. Get elements within lex range with offset and limit + reverse the results.", + presetValues: map[string]interface{}{ + "ZrangeKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "b", Score: 1}, + {Value: "c", Score: 1}, {Value: "d", Score: 1}, + {Value: "e", Score: 1}, {Value: "f", Score: 1}, + {Value: "g", Score: 1}, {Value: "h", Score: 1}, + }), + }, + command: []string{"ZRANGE", "ZrangeKey8", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4", "REV"}, + expectedResponse: [][]string{{"f", "1"}, {"e", "1"}, {"d", "1"}}, + expectedError: nil, + }, + { + name: "9. Return an empty slice when we use BYLEX while elements have different scores", + presetValues: map[string]interface{}{ + "ZrangeKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "b", Score: 5}, + {Value: "c", Score: 2}, {Value: "d", Score: 6}, + {Value: "e", Score: 3}, {Value: "f", Score: 7}, + {Value: "g", Score: 4}, {Value: "h", Score: 8}, + }), + }, + command: []string{"ZRANGE", "ZrangeKey9", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, + expectedResponse: [][]string{}, + expectedError: nil, + }, + { + name: "10. Throw error when limit does not provide both offset and limit", + presetValues: nil, + command: []string{"ZRANGE", "ZrangeKey10", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2"}, + expectedResponse: [][]string{}, + expectedError: errors.New("limit should contain offset and count as integers"), + }, + { + name: "11. Throw error when offset is not a valid integer", + presetValues: nil, + command: []string{"ZRANGE", "ZrangeKey11", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "offset", "4"}, + expectedResponse: [][]string{}, + expectedError: errors.New("limit offset must be integer"), + }, + { + name: "12. Throw error when limit is not a valid integer", + presetValues: nil, + command: []string{"ZRANGE", "ZrangeKey12", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "4", "limit"}, + expectedResponse: [][]string{}, + expectedError: errors.New("limit count must be integer"), + }, + { + name: "13. Throw error when offset is negative", + presetValues: nil, + command: []string{"ZRANGE", "ZrangeKey13", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9"}, + expectedResponse: [][]string{}, + expectedError: errors.New("limit offset must be >= 0"), + }, + { + name: "14. Throw error when the key does not hold a sorted set", + presetValues: map[string]interface{}{ + "ZrangeKey14": "Default value", + }, + command: []string{"ZRANGE", "ZrangeKey14", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, + expectedResponse: [][]string{}, + expectedError: errors.New("value at ZrangeKey14 is not a sorted set"), + }, + { + name: "15. Command too short", + presetValues: nil, + command: []string{"ZRANGE", "ZrangeKey15", "1"}, + expectedResponse: [][]string{}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "16. Command too long", + presetValues: nil, + command: []string{"ZRANGE", "ZrangeKey16", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9", "REV", "WITHSCORES"}, + expectedResponse: [][]string{}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - return - } - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - for i := 0; i < len(res.Array()); i++ { - if test.expectedResponse[i] != res.Array()[i].String() { - t.Errorf("expected element at index %d to be \"%s\", got %s", - i, test.expectedResponse[i], res.Array()[i].String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - } - }) - } -} -func Test_HandleZREM(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedValues map[string]*sorted_set.SortedSet - expectedResponse int - expectedError error - }{ - { - // Successfully remove multiple elements from sorted set, skipping non-existent members. - // Return deleted count. - name: "1. Successfully remove multiple elements from sorted set, skipping non-existent members.", - presetValues: map[string]interface{}{ - "ZremKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - }), - }, - command: []string{"ZREM", "ZremKey1", "three", "four", "five", "none", "six", "none", "seven"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZremKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - }), - }, - expectedResponse: 5, - expectedError: nil, - }, - { - name: "2. If key does not exist, return 0", - presetValues: nil, - command: []string{"ZREM", "ZremKey2", "member"}, - expectedValues: nil, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "3. Return error key is not a sorted set", - presetValues: map[string]interface{}{ - "ZremKey3": "Default value", - }, - command: []string{"ZREM", "ZremKey3", "member"}, - expectedError: errors.New("value at ZremKey3 is not a sorted set"), - }, - { - name: "9. Command too short", - command: []string{"ZREM"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) + } + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) + } } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) } + } + }) + } + }) + + t.Run("Test_HandleZRANGESTORE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + destination string + command []string + expectedValue *sorted_set.SortedSet + expectedResponse int + expectedError error + }{ + { + name: "1. Get elements withing score range without score.", + presetValues: map[string]interface{}{ + "ZrangeStoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + destination: "ZrangeStoreDestinationKey1", + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey1", "ZrangeStoreKey1", "3", "7", "BYSCORE"}, + expectedResponse: 5, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, + {Value: "six", Score: 6}, {Value: "seven", Score: 7}, + }), + expectedError: nil, + }, + { + name: "2. Get elements within score range with score.", + presetValues: map[string]interface{}{ + "ZrangeStoreKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + destination: "ZrangeStoreDestinationKey2", + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey2", "ZrangeStoreKey2", "3", "7", "BYSCORE", "WITHSCORES"}, + expectedResponse: 5, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, + {Value: "six", Score: 6}, {Value: "seven", Score: 7}, + }), + expectedError: nil, + }, + { + // 3. Get elements within score range with offset and limit. + // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). + name: "3. Get elements within score range with offset and limit.", + presetValues: map[string]interface{}{ + "ZrangeStoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + destination: "ZrangeStoreDestinationKey3", + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey3", "ZrangeStoreKey3", "3", "7", "BYSCORE", "WITHSCORES", "LIMIT", "2", "4"}, + expectedResponse: 3, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, + }), + expectedError: nil, + }, + { + // 4. Get elements within score range with offset and limit + reverse the results. + // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). + // REV reverses the original set before getting the range. + name: "4. Get elements within score range with offset and limit + reverse the results.", + presetValues: map[string]interface{}{ + "ZrangeStoreKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + destination: "ZrangeStoreDestinationKey4", + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey4", "ZrangeStoreKey4", "3", "7", "BYSCORE", "WITHSCORES", "LIMIT", "2", "4", "REV"}, + expectedResponse: 3, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "six", Score: 6}, {Value: "five", Score: 5}, {Value: "four", Score: 4}, + }), + expectedError: nil, + }, + { + name: "5. Get elements within lex range without score.", + presetValues: map[string]interface{}{ + "ZrangeStoreKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "e", Score: 1}, + {Value: "b", Score: 1}, {Value: "f", Score: 1}, + {Value: "c", Score: 1}, {Value: "g", Score: 1}, + {Value: "d", Score: 1}, {Value: "h", Score: 1}, + }), + }, + destination: "ZrangeStoreDestinationKey5", + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey5", "ZrangeStoreKey5", "c", "g", "BYLEX"}, + expectedResponse: 5, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "c", Score: 1}, {Value: "d", Score: 1}, {Value: "e", Score: 1}, + {Value: "f", Score: 1}, {Value: "g", Score: 1}, + }), + expectedError: nil, + }, + { + name: "6. Get elements within lex range with score.", + presetValues: map[string]interface{}{ + "ZrangeStoreKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "e", Score: 1}, + {Value: "b", Score: 1}, {Value: "f", Score: 1}, + {Value: "c", Score: 1}, {Value: "g", Score: 1}, + {Value: "d", Score: 1}, {Value: "h", Score: 1}, + }), + }, + destination: "ZrangeStoreDestinationKey6", + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey6", "ZrangeStoreKey6", "a", "f", "BYLEX", "WITHSCORES"}, + expectedResponse: 6, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "b", Score: 1}, {Value: "c", Score: 1}, + {Value: "d", Score: 1}, {Value: "e", Score: 1}, {Value: "f", Score: 1}, + }), + expectedError: nil, + }, + { + // 7. Get elements within lex range with offset and limit. + // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). + name: "7. Get elements within lex range with offset and limit.", + presetValues: map[string]interface{}{ + "ZrangeStoreKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "b", Score: 1}, + {Value: "c", Score: 1}, {Value: "d", Score: 1}, + {Value: "e", Score: 1}, {Value: "f", Score: 1}, + {Value: "g", Score: 1}, {Value: "h", Score: 1}, + }), + }, + destination: "ZrangeStoreDestinationKey7", + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey7", "ZrangeStoreKey7", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, + expectedResponse: 3, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "c", Score: 1}, {Value: "d", Score: 1}, {Value: "e", Score: 1}, + }), + expectedError: nil, + }, + { + // 8. Get elements within lex range with offset and limit + reverse the results. + // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). + // REV reverses the original set before getting the range. + name: "8. Get elements within lex range with offset and limit + reverse the results.", + presetValues: map[string]interface{}{ + "ZrangeStoreKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "b", Score: 1}, + {Value: "c", Score: 1}, {Value: "d", Score: 1}, + {Value: "e", Score: 1}, {Value: "f", Score: 1}, + {Value: "g", Score: 1}, {Value: "h", Score: 1}, + }), + }, + destination: "ZrangeStoreDestinationKey8", + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey8", "ZrangeStoreKey8", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4", "REV"}, + expectedResponse: 3, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "f", Score: 1}, {Value: "e", Score: 1}, {Value: "d", Score: 1}, + }), + expectedError: nil, + }, + { + name: "9. Return an empty slice when we use BYLEX while elements have different scores", + presetValues: map[string]interface{}{ + "ZrangeStoreKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "a", Score: 1}, {Value: "b", Score: 5}, + {Value: "c", Score: 2}, {Value: "d", Score: 6}, + {Value: "e", Score: 3}, {Value: "f", Score: 7}, + {Value: "g", Score: 4}, {Value: "h", Score: 8}, + }), + }, + destination: "ZrangeStoreDestinationKey9", + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey9", "ZrangeStoreKey9", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, + expectedResponse: 0, + expectedValue: nil, + expectedError: nil, + }, + { + name: "10. Throw error when limit does not provide both offset and limit", + presetValues: nil, + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey10", "ZrangeStoreKey10", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2"}, + expectedResponse: 0, + expectedError: errors.New("limit should contain offset and count as integers"), + }, + { + name: "11. Throw error when offset is not a valid integer", + presetValues: nil, + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey11", "ZrangeStoreKey11", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "offset", "4"}, + expectedResponse: 0, + expectedError: errors.New("limit offset must be integer"), + }, + { + name: "12. Throw error when limit is not a valid integer", + presetValues: nil, + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey12", "ZrangeStoreKey12", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "4", "limit"}, + expectedResponse: 0, + expectedError: errors.New("limit count must be integer"), + }, + { + name: "13. Throw error when offset is negative", + presetValues: nil, + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey13", "ZrangeStoreKey13", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9"}, + expectedResponse: 0, + expectedError: errors.New("limit offset must be >= 0"), + }, + { + name: "14. Throw error when the key does not hold a sorted set", + presetValues: map[string]interface{}{ + "ZrangeStoreKey14": "Default value", + }, + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey14", "ZrangeStoreKey14", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, + expectedResponse: 0, + expectedError: errors.New("value at ZrangeStoreKey14 is not a sorted set"), + }, + { + name: "15. Command too short", + presetValues: nil, + command: []string{"ZRANGESTORE", "ZrangeStoreKey15", "1"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "16 Command too long", + presetValues: nil, + command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey16", "ZrangeStoreKey16", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9", "REV", "WITHSCORES"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } - // Check if the resulting sorted set has the expected members/scores - for key, expectedSortedSet := range test.expectedValues { - if expectedSortedSet == nil { - continue + // Check if the resulting sorted set has the expected members/scores + if test.expectedValue == nil { + return } if err = client.WriteArray([]resp.Value{ resp.StringValue("ZRANGE"), - resp.StringValue(key), + resp.StringValue(test.destination), resp.StringValue("-inf"), resp.StringValue("+inf"), resp.StringValue("BYSCORE"), @@ -2751,164 +4084,776 @@ func Test_HandleZREM(t *testing.T) { t.Error(err) } - if len(res.Array()) != expectedSortedSet.Cardinality() { + if len(res.Array()) != test.expectedValue.Cardinality() { t.Errorf("expected resulting set %s to have cardinality %d, got %d", - key, expectedSortedSet.Cardinality(), len(res.Array())) + test.destination, test.expectedValue.Cardinality(), len(res.Array())) } for _, member := range res.Array() { value := sorted_set.Value(member.Array()[0].String()) score := sorted_set.Score(member.Array()[1].Float()) - if !expectedSortedSet.Contains(value) { + if !test.expectedValue.Contains(value) { t.Errorf("unexpected value %s in resulting sorted set", value) } - if expectedSortedSet.Get(value).Score != score { - t.Errorf("expected value %s to have score %v, got %v", - value, expectedSortedSet.Get(value).Score, score) + if test.expectedValue.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) } } - } - }) - } -} - -func Test_HandleZREMRANGEBYSCORE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedValues map[string]*sorted_set.SortedSet - expectedResponse int - expectedError error - }{ - { - name: "1. Successfully remove multiple elements with scores inside the provided range", - presetValues: map[string]interface{}{ - "ZremRangeByScoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - }), - }, - command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey1", "3", "7"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZremRangeByScoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - }), - }, - expectedResponse: 5, - expectedError: nil, - }, - { - name: "2. If key does not exist, return 0", - presetValues: nil, - command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey2", "2", "4"}, - expectedValues: nil, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "3. Return error key is not a sorted set", - presetValues: map[string]interface{}{ - "ZremRangeByScoreKey3": "Default value", - }, - command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey3", "4", "4"}, - expectedError: errors.New("value at ZremRangeByScoreKey3 is not a sorted set"), - }, - { - name: "4. Command too short", - command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey4", "3"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "5. Command too long", - command: []string{"ZREMRANGEBYSCORE", "ZremRangeByScoreKey5", "4", "5", "8"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + }) + } + }) + + t.Run("Test_HandleZINTER", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedResponse [][]string + expectedError error + }{ + { + name: "1. Get the intersection between 2 sorted sets.", + presetValues: map[string]interface{}{ + "ZinterKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + "ZinterKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + command: []string{"ZINTER", "ZinterKey1", "ZinterKey2"}, + expectedResponse: [][]string{{"three"}, {"four"}, {"five"}}, + expectedError: nil, + }, + { + // 2. Get the intersection between 3 sorted sets with scores. + // By default, the SUM aggregate will be used. + name: "2. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 8}, + }), + "ZinterKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZINTER", "ZinterKey3", "ZinterKey4", "ZinterKey5", "WITHSCORES"}, + expectedResponse: [][]string{{"one", "3"}, {"eight", "24"}}, + expectedError: nil, + }, + { + // 3. Get the intersection between 3 sorted sets with scores. + // Use MIN aggregate. + name: "3. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZinterKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZINTER", "ZinterKey6", "ZinterKey7", "ZinterKey8", "WITHSCORES", "AGGREGATE", "MIN"}, + expectedResponse: [][]string{{"one", "1"}, {"eight", "8"}}, + expectedError: nil, + }, + { + // 4. Get the intersection between 3 sorted sets with scores. + // Use MAX aggregate. + name: "4. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZinterKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZINTER", "ZinterKey9", "ZinterKey10", "ZinterKey11", "WITHSCORES", "AGGREGATE", "MAX"}, + expectedResponse: [][]string{{"one", "1000"}, {"eight", "800"}}, + expectedError: nil, + }, + { + // 5. Get the intersection between 3 sorted sets with scores. + // Use SUM aggregate with weights modifier. + name: "5. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZinterKey14": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZINTER", "ZinterKey12", "ZinterKey13", "ZinterKey14", "WITHSCORES", "AGGREGATE", "SUM", "WEIGHTS", "1", "5", "3"}, + expectedResponse: [][]string{{"one", "3105"}, {"eight", "2808"}}, + expectedError: nil, + }, + { + // 6. Get the intersection between 3 sorted sets with scores. + // Use MAX aggregate with added weights. + name: "6. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterKey15": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterKey16": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZinterKey17": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZINTER", "ZinterKey15", "ZinterKey16", "ZinterKey17", "WITHSCORES", "AGGREGATE", "MAX", "WEIGHTS", "1", "5", "3"}, + expectedResponse: [][]string{{"one", "3000"}, {"eight", "2400"}}, + expectedError: nil, + }, + { + // 7. Get the intersection between 3 sorted sets with scores. + // Use MIN aggregate with added weights. + name: "7. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterKey18": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterKey19": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZinterKey20": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZINTER", "ZinterKey18", "ZinterKey19", "ZinterKey20", "WITHSCORES", "AGGREGATE", "MIN", "WEIGHTS", "1", "5", "3"}, + expectedResponse: [][]string{{"one", "5"}, {"eight", "8"}}, + expectedError: nil, + }, + { + name: "8. Throw an error if there are more weights than keys", + presetValues: map[string]interface{}{ + "ZinterKey21": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterKey22": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZINTER", "ZinterKey21", "ZinterKey22", "WEIGHTS", "1", "2", "3"}, + expectedResponse: nil, + expectedError: errors.New("number of weights should match number of keys"), + }, + { + name: "9. Throw an error if there are fewer weights than keys", + presetValues: map[string]interface{}{ + "ZinterKey23": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterKey24": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + }), + "ZinterKey25": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZINTER", "ZinterKey23", "ZinterKey24", "ZinterKey25", "WEIGHTS", "5", "4"}, + expectedResponse: nil, + expectedError: errors.New("number of weights should match number of keys"), + }, + { + name: "10. Throw an error if there are no keys provided", + presetValues: map[string]interface{}{ + "ZinterKey26": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + "ZinterKey27": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + "ZinterKey28": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZINTER", "WEIGHTS", "5", "4"}, + expectedResponse: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "11. Throw an error if any of the provided keys are not sorted sets", + presetValues: map[string]interface{}{ + "ZinterKey29": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterKey30": "Default value", + "ZinterKey31": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZINTER", "ZinterKey29", "ZinterKey30", "ZinterKey31"}, + expectedResponse: nil, + expectedError: errors.New("value at ZinterKey30 is not a sorted set"), + }, + { + name: "12. If any of the keys does not exist, return an empty array.", + presetValues: map[string]interface{}{ + "ZinterKey32": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, + }), + "ZinterKey33": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZINTER", "non-existent", "ZinterKey32", "ZinterKey33"}, + expectedResponse: [][]string{}, + expectedError: nil, + }, + { + name: "13. Command too short", + command: []string{"ZINTER"}, + expectedResponse: [][]string{}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) - } - if err = client.WriteArray(command); err != nil { - t.Error(err) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) + + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } + return + } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) + } + + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() + } + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) + } + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) + } + } } } + }) + } + }) + + t.Run("Test_HandleZINTERSTORE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + destination string + command []string + expectedValue *sorted_set.SortedSet + expectedResponse int + expectedError error + }{ + { + name: "1. Get the intersection between 2 sorted sets.", + presetValues: map[string]interface{}{ + "ZinterStoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + "ZinterStoreKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + destination: "ZinterStoreDestinationKey1", + command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey1", "ZinterStoreKey1", "ZinterStoreKey2"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 6}, {Value: "four", Score: 8}, + {Value: "five", Score: 10}, + }), + expectedResponse: 3, + expectedError: nil, + }, + { + // 2. Get the intersection between 3 sorted sets with scores. + // By default, the SUM aggregate will be used. + name: "2. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterStoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterStoreKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 8}, + }), + "ZinterStoreKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZinterStoreDestinationKey2", + command: []string{ + "ZINTERSTORE", "ZinterStoreDestinationKey2", "ZinterStoreKey3", "ZinterStoreKey4", "ZinterStoreKey5", "WITHSCORES", + }, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 3}, {Value: "eight", Score: 24}, + }), + expectedResponse: 2, + expectedError: nil, + }, + { + // 3. Get the intersection between 3 sorted sets with scores. + // Use MIN aggregate. + name: "3. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterStoreKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterStoreKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZinterStoreKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZinterStoreDestinationKey3", + command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey3", "ZinterStoreKey6", "ZinterStoreKey7", "ZinterStoreKey8", "WITHSCORES", "AGGREGATE", "MIN"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "eight", Score: 8}, + }), + expectedResponse: 2, + expectedError: nil, + }, + { + // 4. Get the intersection between 3 sorted sets with scores. + // Use MAX aggregate. + name: "4. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterStoreKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterStoreKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZinterStoreKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZinterStoreDestinationKey4", + command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey4", "ZinterStoreKey9", "ZinterStoreKey10", "ZinterStoreKey11", "WITHSCORES", "AGGREGATE", "MAX"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + }), + expectedResponse: 2, + expectedError: nil, + }, + { + // 5. Get the intersection between 3 sorted sets with scores. + // Use SUM aggregate with weights modifier. + name: "5. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterStoreKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterStoreKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZinterStoreKey14": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZinterStoreDestinationKey5", + command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey5", "ZinterStoreKey12", "ZinterStoreKey13", "ZinterStoreKey14", "WITHSCORES", "AGGREGATE", "SUM", "WEIGHTS", "1", "5", "3"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 3105}, {Value: "eight", Score: 2808}, + }), + expectedResponse: 2, + expectedError: nil, + }, + { + // 6. Get the intersection between 3 sorted sets with scores. + // Use MAX aggregate with added weights. + name: "6. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterStoreKey15": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterStoreKey16": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZinterStoreKey17": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZinterStoreDestinationKey6", + command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey6", "ZinterStoreKey15", "ZinterStoreKey16", "ZinterStoreKey17", "WITHSCORES", "AGGREGATE", "MAX", "WEIGHTS", "1", "5", "3"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 3000}, {Value: "eight", Score: 2400}, + }), + expectedResponse: 2, + expectedError: nil, + }, + { + // 7. Get the intersection between 3 sorted sets with scores. + // Use MIN aggregate with added weights. + name: "7. Get the intersection between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZinterStoreKey18": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterStoreKey19": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZinterStoreKey20": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZinterStoreDestinationKey7", + command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey7", "ZinterStoreKey18", "ZinterStoreKey19", "ZinterStoreKey20", "WITHSCORES", "AGGREGATE", "MIN", "WEIGHTS", "1", "5", "3"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 5}, {Value: "eight", Score: 8}, + }), + expectedResponse: 2, + expectedError: nil, + }, + { + name: "8. Throw an error if there are more weights than keys", + presetValues: map[string]interface{}{ + "ZinterStoreKey21": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterStoreKey22": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey8", "ZinterStoreKey21", "ZinterStoreKey22", "WEIGHTS", "1", "2", "3"}, + expectedResponse: 0, + expectedError: errors.New("number of weights should match number of keys"), + }, + { + name: "9. Throw an error if there are fewer weights than keys", + presetValues: map[string]interface{}{ + "ZinterStoreKey23": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterStoreKey24": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + }), + "ZinterStoreKey25": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey9", "ZinterStoreKey23", "ZinterStoreKey24", "ZinterStoreKey25", "WEIGHTS", "5", "4"}, + expectedResponse: 0, + expectedError: errors.New("number of weights should match number of keys"), + }, + { + name: "10. Throw an error if there are no keys provided", + presetValues: map[string]interface{}{ + "ZinterStoreKey26": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + "ZinterStoreKey27": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + "ZinterStoreKey28": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZINTERSTORE", "WEIGHTS", "5", "4"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "11. Throw an error if any of the provided keys are not sorted sets", + presetValues: map[string]interface{}{ + "ZinterStoreKey29": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZinterStoreKey30": "Default value", + "ZinterStoreKey31": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZINTERSTORE", "ZinterStoreKey29", "ZinterStoreKey30", "ZinterStoreKey31"}, + expectedResponse: 0, + expectedError: errors.New("value at ZinterStoreKey30 is not a sorted set"), + }, + { + name: "12. If any of the keys does not exist, return an empty array.", + presetValues: map[string]interface{}{ + "ZinterStoreKey32": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, + }), + "ZinterStoreKey33": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey12", "non-existent", "ZinterStoreKey32", "ZinterStoreKey33"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "13. Command too short", + command: []string{"ZINTERSTORE"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } + + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - } + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } + } + } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } - // Check if the resulting sorted set has the expected members/scores - for key, expectedSortedSet := range test.expectedValues { - if expectedSortedSet == nil { - continue + // Check if the resulting sorted set has the expected members/scores + if test.expectedValue == nil { + return } if err = client.WriteArray([]resp.Value{ resp.StringValue("ZRANGE"), - resp.StringValue(key), + resp.StringValue(test.destination), resp.StringValue("-inf"), resp.StringValue("+inf"), resp.StringValue("BYSCORE"), @@ -2922,414 +4867,852 @@ func Test_HandleZREMRANGEBYSCORE(t *testing.T) { t.Error(err) } - if len(res.Array()) != expectedSortedSet.Cardinality() { + if len(res.Array()) != test.expectedValue.Cardinality() { t.Errorf("expected resulting set %s to have cardinality %d, got %d", - key, expectedSortedSet.Cardinality(), len(res.Array())) + test.destination, test.expectedValue.Cardinality(), len(res.Array())) } for _, member := range res.Array() { value := sorted_set.Value(member.Array()[0].String()) score := sorted_set.Score(member.Array()[1].Float()) - if !expectedSortedSet.Contains(value) { + if !test.expectedValue.Contains(value) { t.Errorf("unexpected value %s in resulting sorted set", value) } - if expectedSortedSet.Get(value).Score != score { - t.Errorf("expected value %s to have score %v, got %v", - value, expectedSortedSet.Get(value).Score, score) + if test.expectedValue.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) } } - } - }) - } -} - -func Test_HandleZREMRANGEBYRANK(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedValues map[string]*sorted_set.SortedSet - expectedResponse int - expectedError error - }{ - { - name: "1. Successfully remove multiple elements within range", - presetValues: map[string]interface{}{ - "ZremRangeByRankKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - }), - }, - command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey1", "0", "5"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZremRangeByRankKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - }), - }, - expectedResponse: 6, - expectedError: nil, - }, - { - name: "2. Establish boundaries from the end of the set when negative boundaries are provided", - presetValues: map[string]interface{}{ - "ZremRangeByRankKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - }), - }, - command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey2", "-6", "-3"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZremRangeByRankKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - }), - }, - expectedResponse: 4, - expectedError: nil, - }, - { - name: "3. If key does not exist, return 0", - presetValues: nil, - command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey3", "2", "4"}, - expectedValues: nil, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "4. Return error key is not a sorted set", - presetValues: map[string]interface{}{ - "ZremRangeByRankKey3": "Default value", - }, - command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey3", "4", "4"}, - expectedError: errors.New("value at ZremRangeByRankKey3 is not a sorted set"), - }, - { - name: "5. Return error when start index is out of bounds", - presetValues: map[string]interface{}{ - "ZremRangeByRankKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - }), - }, - command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey5", "-12", "5"}, - expectedValues: nil, - expectedResponse: 0, - expectedError: errors.New("indices out of bounds"), - }, - { - name: "6. Return error when end index is out of bounds", - presetValues: map[string]interface{}{ - "ZremRangeByRankKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - }), - }, - command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey6", "0", "11"}, - expectedValues: nil, - expectedResponse: 0, - expectedError: errors.New("indices out of bounds"), - }, - { - name: "7. Command too short", - command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey4", "3"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "8. Command too long", - command: []string{"ZREMRANGEBYRANK", "ZremRangeByRankKey7", "4", "5", "8"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + }) + } + }) + + t.Run("Test_HandleZUNION", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + presetValues map[string]interface{} + command []string + expectedResponse [][]string + expectedError error + }{ + { + name: "1. Get the union between 2 sorted sets.", + presetValues: map[string]interface{}{ + "ZunionKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + "ZunionKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + command: []string{"ZUNION", "ZunionKey1", "ZunionKey2"}, + expectedResponse: [][]string{{"one"}, {"two"}, {"three"}, {"four"}, {"five"}, {"six"}, {"seven"}, {"eight"}}, + expectedError: nil, + }, + { + // 2. Get the union between 3 sorted sets with scores. + // By default, the SUM aggregate will be used. + name: "2. Get the union between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZunionKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 8}, + }), + "ZunionKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 36}, + }), + }, + command: []string{"ZUNION", "ZunionKey3", "ZunionKey4", "ZunionKey5", "WITHSCORES"}, + expectedResponse: [][]string{ + {"one", "3"}, {"two", "4"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, + {"seven", "7"}, {"eight", "24"}, {"nine", "9"}, {"ten", "10"}, {"eleven", "11"}, + {"twelve", "24"}, {"thirty-six", "72"}, + }, + expectedError: nil, + }, + { + // 3. Get the union between 3 sorted sets with scores. + // Use MIN aggregate. + name: "3. Get the union between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZunionKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZunionKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 72}, + }), + }, + command: []string{"ZUNION", "ZunionKey6", "ZunionKey7", "ZunionKey8", "WITHSCORES", "AGGREGATE", "MIN"}, + expectedResponse: [][]string{ + {"one", "1"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, + {"seven", "7"}, {"eight", "8"}, {"nine", "9"}, {"ten", "10"}, {"eleven", "11"}, + {"twelve", "12"}, {"thirty-six", "36"}, + }, + expectedError: nil, + }, + { + // 4. Get the union between 3 sorted sets with scores. + // Use MAX aggregate. + name: "4. Get the union between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZunionKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZunionKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 72}, + }), + }, + command: []string{"ZUNION", "ZunionKey9", "ZunionKey10", "ZunionKey11", "WITHSCORES", "AGGREGATE", "MAX"}, + expectedResponse: [][]string{ + {"one", "1000"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, + {"seven", "7"}, {"eight", "800"}, {"nine", "9"}, {"ten", "10"}, {"eleven", "11"}, + {"twelve", "12"}, {"thirty-six", "72"}, + }, + expectedError: nil, + }, + { + // 5. Get the union between 3 sorted sets with scores. + // Use SUM aggregate with weights modifier. + name: "5. Get the union between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZunionKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZunionKey14": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZUNION", "ZunionKey12", "ZunionKey13", "ZunionKey14", "WITHSCORES", "AGGREGATE", "SUM", "WEIGHTS", "1", "2", "3"}, + expectedResponse: [][]string{ + {"one", "3102"}, {"two", "6"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, + {"seven", "7"}, {"eight", "2568"}, {"nine", "27"}, {"ten", "30"}, {"eleven", "22"}, + {"twelve", "60"}, {"thirty-six", "72"}, + }, + expectedError: nil, + }, + { + // 6. Get the union between 3 sorted sets with scores. + // Use MAX aggregate with added weights. + name: "6. Get the union between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZunionKey15": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionKey16": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZunionKey17": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZUNION", "ZunionKey15", "ZunionKey16", "ZunionKey17", "WITHSCORES", "AGGREGATE", "MAX", "WEIGHTS", "1", "2", "3"}, + expectedResponse: [][]string{ + {"one", "3000"}, {"two", "4"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, + {"seven", "7"}, {"eight", "2400"}, {"nine", "27"}, {"ten", "30"}, {"eleven", "22"}, + {"twelve", "36"}, {"thirty-six", "72"}, + }, + expectedError: nil, + }, + { + // 7. Get the union between 3 sorted sets with scores. + // Use MIN aggregate with added weights. + name: "7. Get the union between 3 sorted sets with scores.", + presetValues: map[string]interface{}{ + "ZunionKey18": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionKey19": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZunionKey20": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZUNION", "ZunionKey18", "ZunionKey19", "ZunionKey20", "WITHSCORES", "AGGREGATE", "MIN", "WEIGHTS", "1", "2", "3"}, + expectedResponse: [][]string{ + {"one", "2"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, {"seven", "7"}, + {"eight", "8"}, {"nine", "27"}, {"ten", "30"}, {"eleven", "22"}, {"twelve", "24"}, {"thirty-six", "72"}, + }, + expectedError: nil, + }, + { + name: "8. Throw an error if there are more weights than keys", + presetValues: map[string]interface{}{ + "ZunionKey21": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionKey22": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZUNION", "ZunionKey21", "ZunionKey22", "WEIGHTS", "1", "2", "3"}, + expectedResponse: nil, + expectedError: errors.New("number of weights should match number of keys"), + }, + { + name: "9. Throw an error if there are fewer weights than keys", + presetValues: map[string]interface{}{ + "ZunionKey23": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionKey24": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + }), + "ZunionKey25": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZUNION", "ZunionKey23", "ZunionKey24", "ZunionKey25", "WEIGHTS", "5", "4"}, + expectedResponse: nil, + expectedError: errors.New("number of weights should match number of keys"), + }, + { + name: "10. Throw an error if there are no keys provided", + presetValues: map[string]interface{}{ + "ZunionKey26": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + "ZunionKey27": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + "ZunionKey28": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZUNION", "WEIGHTS", "5", "4"}, + expectedResponse: nil, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "11. Throw an error if any of the provided keys are not sorted sets", + presetValues: map[string]interface{}{ + "ZunionKey29": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionKey30": "Default value", + "ZunionKey31": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZUNION", "ZunionKey29", "ZunionKey30", "ZunionKey31"}, + expectedResponse: nil, + expectedError: errors.New("value at ZunionKey30 is not a sorted set"), + }, + { + name: "12. If any of the keys does not exist, skip it.", + presetValues: map[string]interface{}{ + "ZunionKey32": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, + }), + "ZunionKey33": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + command: []string{"ZUNION", "non-existent", "ZunionKey32", "ZunionKey33"}, + expectedResponse: [][]string{ + {"one"}, {"two"}, {"thirty-six"}, {"twelve"}, {"eleven"}, + {"seven"}, {"eight"}, {"nine"}, {"ten"}, + }, + expectedError: nil, + }, + { + name: "13. Command too short", + command: []string{"ZUNION"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) - } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) - } - - // Check if the resulting sorted set has the expected members/scores - for key, expectedSortedSet := range test.expectedValues { - if expectedSortedSet == nil { - continue + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) } - if err = client.WriteArray([]resp.Value{ - resp.StringValue("ZRANGE"), - resp.StringValue(key), - resp.StringValue("-inf"), - resp.StringValue("+inf"), - resp.StringValue("BYSCORE"), - resp.StringValue("WITHSCORES"), - }); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } - - res, _, err = client.ReadValue() + res, _, err := client.ReadValue() if err != nil { t.Error(err) } - if len(res.Array()) != expectedSortedSet.Cardinality() { - t.Errorf("expected resulting set %s to have cardinality %d, got %d", - key, expectedSortedSet.Cardinality(), len(res.Array())) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - for _, member := range res.Array() { - value := sorted_set.Value(member.Array()[0].String()) - score := sorted_set.Score(member.Array()[1].Float()) - if !expectedSortedSet.Contains(value) { - t.Errorf("unexpected value %s in resulting sorted set", value) + if len(res.Array()) != len(test.expectedResponse) { + t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) + } + + for _, item := range res.Array() { + value := item.Array()[0].String() + score := func() string { + if len(item.Array()) == 2 { + return item.Array()[1].String() + } + return "" + }() + if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { + return expected[0] == value + }) { + t.Errorf("unexpected member \"%s\" in response", value) } - if expectedSortedSet.Get(value).Score != score { - t.Errorf("expected value %s to have score %v, got %v", - value, expectedSortedSet.Get(value).Score, score) + if score != "" { + for _, expected := range test.expectedResponse { + if expected[0] == value && expected[1] != score { + t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) + } + } } } - } - }) - } -} - -func Test_HandleZREMRANGEBYLEX(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedValues map[string]*sorted_set.SortedSet - expectedResponse int - expectedError error - }{ - { - name: "1. Successfully remove multiple elements with scores inside the provided range", - presetValues: map[string]interface{}{ - "ZremRangeByLexKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "b", Score: 1}, - {Value: "c", Score: 1}, {Value: "d", Score: 1}, - {Value: "e", Score: 1}, {Value: "f", Score: 1}, - {Value: "g", Score: 1}, {Value: "h", Score: 1}, - {Value: "i", Score: 1}, {Value: "j", Score: 1}, - }), - }, - command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey1", "a", "d"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZremRangeByLexKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "e", Score: 1}, {Value: "f", Score: 1}, - {Value: "g", Score: 1}, {Value: "h", Score: 1}, - {Value: "i", Score: 1}, {Value: "j", Score: 1}, + }) + } + }) + + t.Run("Test_HandleZUNIONSTORE", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error() + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) + + tests := []struct { + name string + preset bool + presetValues map[string]interface{} + destination string + command []string + expectedValue *sorted_set.SortedSet + expectedResponse int + expectedError error + }{ + { + name: "1. Get the union between 2 sorted sets.", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, + }), + "ZunionStoreKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + }, + destination: "ZunionStoreDestinationKey1", + command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey1", "ZunionStoreKey1", "ZunionStoreKey2"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 6}, {Value: "four", Score: 8}, + {Value: "five", Score: 10}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, }), - }, - expectedResponse: 4, - expectedError: nil, - }, - { - name: "2. Return 0 if the members do not have the same score", - presetValues: map[string]interface{}{ - "ZremRangeByLexKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "b", Score: 2}, - {Value: "c", Score: 3}, {Value: "d", Score: 4}, - {Value: "e", Score: 5}, {Value: "f", Score: 6}, - {Value: "g", Score: 7}, {Value: "h", Score: 8}, - {Value: "i", Score: 9}, {Value: "j", Score: 10}, + expectedResponse: 8, + expectedError: nil, + }, + { + // 2. Get the union between 3 sorted sets with scores. + // By default, the SUM aggregate will be used. + name: "2. Get the union between 3 sorted sets with scores.", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionStoreKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 8}, + }), + "ZunionStoreKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 36}, + }), + }, + destination: "ZunionStoreDestinationKey2", + command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey2", "ZunionStoreKey3", "ZunionStoreKey4", "ZunionStoreKey5", "WITHSCORES"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 3}, {Value: "two", Score: 4}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 24}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "eleven", Score: 11}, + {Value: "twelve", Score: 24}, {Value: "thirty-six", Score: 72}, + }), + expectedResponse: 13, + expectedError: nil, + }, + { + // 3. Get the union between 3 sorted sets with scores. + // Use MIN aggregate. + name: "3. Get the union between 3 sorted sets with scores.", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionStoreKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZunionStoreKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 72}, + }), + }, + destination: "ZunionStoreDestinationKey3", + command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey3", "ZunionStoreKey6", "ZunionStoreKey7", "ZunionStoreKey8", "WITHSCORES", "AGGREGATE", "MIN"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "eleven", Score: 11}, + {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 36}, }), - }, - command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey2", "d", "g"}, - expectedValues: map[string]*sorted_set.SortedSet{ - "ZremRangeByLexKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "b", Score: 2}, - {Value: "c", Score: 3}, {Value: "d", Score: 4}, - {Value: "e", Score: 5}, {Value: "f", Score: 6}, - {Value: "g", Score: 7}, {Value: "h", Score: 8}, - {Value: "i", Score: 9}, {Value: "j", Score: 10}, + expectedResponse: 13, + expectedError: nil, + }, + { + // 4. Get the union between 3 sorted sets with scores. + // Use MAX aggregate. + name: "4. Get the union between 3 sorted sets with scores.", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionStoreKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZunionStoreKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 72}, + }), + }, + destination: "ZunionStoreDestinationKey4", + command: []string{ + "ZUNIONSTORE", "ZunionStoreDestinationKey4", "ZunionStoreKey9", "ZunionStoreKey10", "ZunionStoreKey11", "WITHSCORES", "AGGREGATE", "MAX", + }, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "eleven", Score: 11}, + {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 72}, }), - }, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "3. If key does not exist, return 0", - presetValues: nil, - command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey3", "2", "4"}, - expectedValues: nil, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "4. Return error key is not a sorted set", - presetValues: map[string]interface{}{ - "ZremRangeByLexKey3": "Default value", - }, - command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey3", "a", "d"}, - expectedError: errors.New("value at ZremRangeByLexKey3 is not a sorted set"), - }, - { - name: "5. Command too short", - command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey4", "a"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "6. Command too long", - command: []string{"ZREMRANGEBYLEX", "ZremRangeByLexKey5", "a", "b", "c"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + expectedResponse: 13, + expectedError: nil, + }, + { + // 5. Get the union between 3 sorted sets with scores. + // Use SUM aggregate with weights modifier. + name: "5. Get the union between 3 sorted sets with scores.", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionStoreKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZunionStoreKey14": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZunionStoreDestinationKey5", + command: []string{ + "ZUNIONSTORE", "ZunionStoreDestinationKey5", "ZunionStoreKey12", "ZunionStoreKey13", "ZunionStoreKey14", + "WITHSCORES", "AGGREGATE", "SUM", "WEIGHTS", "1", "2", "3", + }, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 3102}, {Value: "two", Score: 6}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 2568}, + {Value: "nine", Score: 27}, {Value: "ten", Score: 30}, {Value: "eleven", Score: 22}, + {Value: "twelve", Score: 60}, {Value: "thirty-six", Score: 72}, + }), + expectedResponse: 13, + expectedError: nil, + }, + { + // 6. Get the union between 3 sorted sets with scores. + // Use MAX aggregate with added weights. + name: "6. Get the union between 3 sorted sets with scores.", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey15": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionStoreKey16": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZunionStoreKey17": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZunionStoreDestinationKey6", + command: []string{ + "ZUNIONSTORE", "ZunionStoreDestinationKey6", "ZunionStoreKey15", "ZunionStoreKey16", "ZunionStoreKey17", + "WITHSCORES", "AGGREGATE", "MAX", "WEIGHTS", "1", "2", "3"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 3000}, {Value: "two", Score: 4}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 2400}, + {Value: "nine", Score: 27}, {Value: "ten", Score: 30}, {Value: "eleven", Score: 22}, + {Value: "twelve", Score: 36}, {Value: "thirty-six", Score: 72}, + }), + expectedResponse: 13, + expectedError: nil, + }, + { + // 7. Get the union between 3 sorted sets with scores. + // Use MIN aggregate with added weights. + name: "7. Get the union between 3 sorted sets with scores.", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey18": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 100}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionStoreKey19": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, + }), + "ZunionStoreKey20": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZunionStoreDestinationKey7", + command: []string{ + "ZUNIONSTORE", "ZunionStoreDestinationKey7", "ZunionStoreKey18", "ZunionStoreKey19", "ZunionStoreKey20", + "WITHSCORES", "AGGREGATE", "MIN", "WEIGHTS", "1", "2", "3", + }, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 2}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 27}, {Value: "ten", Score: 30}, {Value: "eleven", Score: 22}, + {Value: "twelve", Score: 24}, {Value: "thirty-six", Score: 72}, + }), + expectedResponse: 13, + expectedError: nil, + }, + { + name: "8. Throw an error if there are more weights than keys", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey21": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionStoreKey22": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + destination: "ZunionStoreDestinationKey8", + command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey8", "ZunionStoreKey21", "ZunionStoreKey22", "WEIGHTS", "1", "2", "3"}, + expectedResponse: 0, + expectedError: errors.New("number of weights should match number of keys"), + }, + { + name: "9. Throw an error if there are fewer weights than keys", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey23": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionStoreKey24": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + }), + "ZunionStoreKey25": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + destination: "ZunionStoreDestinationKey9", + command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey9", "ZunionStoreKey23", "ZunionStoreKey24", "ZunionStoreKey25", "WEIGHTS", "5", "4"}, + expectedResponse: 0, + expectedError: errors.New("number of weights should match number of keys"), + }, + { + name: "10. Throw an error if there are no keys provided", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey26": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + "ZunionStoreKey27": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + "ZunionStoreKey28": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + command: []string{"ZUNIONSTORE", "WEIGHTS", "5", "4"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "11. Throw an error if any of the provided keys are not sorted sets", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey29": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "three", Score: 3}, {Value: "four", Score: 4}, + {Value: "five", Score: 5}, {Value: "six", Score: 6}, + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + }), + "ZunionStoreKey30": "Default value", + "ZunionStoreKey31": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), + }, + destination: "ZunionStoreDestinationKey11", + command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey11", "ZunionStoreKey29", "ZunionStoreKey30", "ZunionStoreKey31"}, + expectedResponse: 0, + expectedError: errors.New("value at ZunionStoreKey30 is not a sorted set"), + }, + { + name: "12. If any of the keys does not exist, skip it.", + preset: true, + presetValues: map[string]interface{}{ + "ZunionStoreKey32": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, + {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, + {Value: "eleven", Score: 11}, + }), + "ZunionStoreKey33": sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, + {Value: "twelve", Score: 12}, + }), + }, + destination: "ZunionStoreDestinationKey12", + command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey12", "non-existent", "ZunionStoreKey32", "ZunionStoreKey33"}, + expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ + {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, + {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "eleven", Score: 11}, {Value: "twelve", Score: 24}, + {Value: "thirty-six", Score: 36}, + }), + expectedResponse: 9, + expectedError: nil, + }, + { + name: "13. Command too short", + preset: false, + command: []string{"ZUNIONSTORE"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValues != nil { + var command []resp.Value + var expected string + for key, value := range test.presetValues { + switch value.(type) { + case string: + command = []resp.Value{ + resp.StringValue("SET"), + resp.StringValue(key), + resp.StringValue(value.(string)), + } + expected = "ok" + case *sorted_set.SortedSet: + command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} + for _, member := range value.(*sorted_set.SortedSet).GetAll() { + command = append(command, []resp.Value{ + resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), + resp.StringValue(string(member.Value)), + }...) + } + expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), + if err = client.WriteArray(command); err != nil { + t.Error(err) } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if !strings.EqualFold(res.String(), expected) { + t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + } } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + if err = client.WriteArray(command); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) + } + return } - return - } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response array of length %d, got %d", test.expectedResponse, res.Integer()) - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) + } - // Check if the resulting sorted set has the expected members/scores - for key, expectedSortedSet := range test.expectedValues { - if expectedSortedSet == nil { - continue + // Check if the resulting sorted set has the expected members/scores + if test.expectedValue == nil { + return } if err = client.WriteArray([]resp.Value{ resp.StringValue("ZRANGE"), - resp.StringValue(key), + resp.StringValue(test.destination), resp.StringValue("-inf"), resp.StringValue("+inf"), resp.StringValue("BYSCORE"), @@ -3343,2287 +5726,22 @@ func Test_HandleZREMRANGEBYLEX(t *testing.T) { t.Error(err) } - if len(res.Array()) != expectedSortedSet.Cardinality() { + if len(res.Array()) != test.expectedValue.Cardinality() { t.Errorf("expected resulting set %s to have cardinality %d, got %d", - key, expectedSortedSet.Cardinality(), len(res.Array())) + test.destination, test.expectedValue.Cardinality(), len(res.Array())) } for _, member := range res.Array() { value := sorted_set.Value(member.Array()[0].String()) score := sorted_set.Score(member.Array()[1].Float()) - if !expectedSortedSet.Contains(value) { + if !test.expectedValue.Contains(value) { t.Errorf("unexpected value %s in resulting sorted set", value) } - if expectedSortedSet.Get(value).Score != score { - t.Errorf("expected value %s to have score %v, got %v", - value, expectedSortedSet.Get(value).Score, score) - } - } - } - }) - } -} - -func Test_HandleZRANGE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedResponse [][]string - expectedError error - }{ - { - name: "1. Get elements withing score range without score.", - presetValues: map[string]interface{}{ - "ZrangeKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - command: []string{"ZRANGE", "ZrangeKey1", "3", "7", "BYSCORE"}, - expectedResponse: [][]string{{"three"}, {"four"}, {"five"}, {"six"}, {"seven"}}, - expectedError: nil, - }, - { - name: "2. Get elements within score range with score.", - presetValues: map[string]interface{}{ - "ZrangeKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - command: []string{"ZRANGE", "ZrangeKey2", "3", "7", "BYSCORE", "WITHSCORES"}, - expectedResponse: [][]string{ - {"three", "3"}, {"four", "4"}, {"five", "5"}, - {"six", "6"}, {"seven", "7"}}, - expectedError: nil, - }, - { - // 3. Get elements within score range with offset and limit. - // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - name: "3. Get elements within score range with offset and limit.", - presetValues: map[string]interface{}{ - "ZrangeKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - command: []string{"ZRANGE", "ZrangeKey3", "3", "7", "BYSCORE", "WITHSCORES", "LIMIT", "2", "4"}, - expectedResponse: [][]string{{"three", "3"}, {"four", "4"}, {"five", "5"}}, - expectedError: nil, - }, - { - // 4. Get elements within score range with offset and limit + reverse the results. - // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - // REV reverses the original set before getting the range. - name: "4. Get elements within score range with offset and limit + reverse the results.", - presetValues: map[string]interface{}{ - "ZrangeKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - command: []string{"ZRANGE", "ZrangeKey4", "3", "7", "BYSCORE", "WITHSCORES", "LIMIT", "2", "4", "REV"}, - expectedResponse: [][]string{{"six", "6"}, {"five", "5"}, {"four", "4"}}, - expectedError: nil, - }, - { - name: "5. Get elements within lex range without score.", - presetValues: map[string]interface{}{ - "ZrangeKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "e", Score: 1}, - {Value: "b", Score: 1}, {Value: "f", Score: 1}, - {Value: "c", Score: 1}, {Value: "g", Score: 1}, - {Value: "d", Score: 1}, {Value: "h", Score: 1}, - }), - }, - command: []string{"ZRANGE", "ZrangeKey5", "c", "g", "BYLEX"}, - expectedResponse: [][]string{{"c"}, {"d"}, {"e"}, {"f"}, {"g"}}, - expectedError: nil, - }, - { - name: "6. Get elements within lex range with score.", - presetValues: map[string]interface{}{ - "ZrangeKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "e", Score: 1}, - {Value: "b", Score: 1}, {Value: "f", Score: 1}, - {Value: "c", Score: 1}, {Value: "g", Score: 1}, - {Value: "d", Score: 1}, {Value: "h", Score: 1}, - }), - }, - command: []string{"ZRANGE", "ZrangeKey6", "a", "f", "BYLEX", "WITHSCORES"}, - expectedResponse: [][]string{ - {"a", "1"}, {"b", "1"}, {"c", "1"}, - {"d", "1"}, {"e", "1"}, {"f", "1"}}, - expectedError: nil, - }, - { - // 7. Get elements within lex range with offset and limit. - // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - name: "7. Get elements within lex range with offset and limit.", - presetValues: map[string]interface{}{ - "ZrangeKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "b", Score: 1}, - {Value: "c", Score: 1}, {Value: "d", Score: 1}, - {Value: "e", Score: 1}, {Value: "f", Score: 1}, - {Value: "g", Score: 1}, {Value: "h", Score: 1}, - }), - }, - command: []string{"ZRANGE", "ZrangeKey7", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, - expectedResponse: [][]string{{"c", "1"}, {"d", "1"}, {"e", "1"}}, - expectedError: nil, - }, - { - // 8. Get elements within lex range with offset and limit + reverse the results. - // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - // REV reverses the original set before getting the range. - name: "8. Get elements within lex range with offset and limit + reverse the results.", - presetValues: map[string]interface{}{ - "ZrangeKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "b", Score: 1}, - {Value: "c", Score: 1}, {Value: "d", Score: 1}, - {Value: "e", Score: 1}, {Value: "f", Score: 1}, - {Value: "g", Score: 1}, {Value: "h", Score: 1}, - }), - }, - command: []string{"ZRANGE", "ZrangeKey8", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4", "REV"}, - expectedResponse: [][]string{{"f", "1"}, {"e", "1"}, {"d", "1"}}, - expectedError: nil, - }, - { - name: "9. Return an empty slice when we use BYLEX while elements have different scores", - presetValues: map[string]interface{}{ - "ZrangeKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "b", Score: 5}, - {Value: "c", Score: 2}, {Value: "d", Score: 6}, - {Value: "e", Score: 3}, {Value: "f", Score: 7}, - {Value: "g", Score: 4}, {Value: "h", Score: 8}, - }), - }, - command: []string{"ZRANGE", "ZrangeKey9", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, - expectedResponse: [][]string{}, - expectedError: nil, - }, - { - name: "10. Throw error when limit does not provide both offset and limit", - presetValues: nil, - command: []string{"ZRANGE", "ZrangeKey10", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2"}, - expectedResponse: [][]string{}, - expectedError: errors.New("limit should contain offset and count as integers"), - }, - { - name: "11. Throw error when offset is not a valid integer", - presetValues: nil, - command: []string{"ZRANGE", "ZrangeKey11", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "offset", "4"}, - expectedResponse: [][]string{}, - expectedError: errors.New("limit offset must be integer"), - }, - { - name: "12. Throw error when limit is not a valid integer", - presetValues: nil, - command: []string{"ZRANGE", "ZrangeKey12", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "4", "limit"}, - expectedResponse: [][]string{}, - expectedError: errors.New("limit count must be integer"), - }, - { - name: "13. Throw error when offset is negative", - presetValues: nil, - command: []string{"ZRANGE", "ZrangeKey13", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9"}, - expectedResponse: [][]string{}, - expectedError: errors.New("limit offset must be >= 0"), - }, - { - name: "14. Throw error when the key does not hold a sorted set", - presetValues: map[string]interface{}{ - "ZrangeKey14": "Default value", - }, - command: []string{"ZRANGE", "ZrangeKey14", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, - expectedResponse: [][]string{}, - expectedError: errors.New("value at ZrangeKey14 is not a sorted set"), - }, - { - name: "15. Command too short", - presetValues: nil, - command: []string{"ZRANGE", "ZrangeKey15", "1"}, - expectedResponse: [][]string{}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "16. Command too long", - presetValues: nil, - command: []string{"ZRANGE", "ZrangeKey16", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9", "REV", "WITHSCORES"}, - expectedResponse: [][]string{}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), - } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) - } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) - } - return - } - - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) - } - - for _, item := range res.Array() { - value := item.Array()[0].String() - score := func() string { - if len(item.Array()) == 2 { - return item.Array()[1].String() - } - return "" - }() - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - return expected[0] == value - }) { - t.Errorf("unexpected member \"%s\" in response", value) - } - if score != "" { - for _, expected := range test.expectedResponse { - if expected[0] == value && expected[1] != score { - t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) - } - } - } - } - }) - } -} - -func Test_HandleZRANGESTORE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - destination string - command []string - expectedValue *sorted_set.SortedSet - expectedResponse int - expectedError error - }{ - { - name: "1. Get elements withing score range without score.", - presetValues: map[string]interface{}{ - "ZrangeStoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - destination: "ZrangeStoreDestinationKey1", - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey1", "ZrangeStoreKey1", "3", "7", "BYSCORE"}, - expectedResponse: 5, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, - {Value: "six", Score: 6}, {Value: "seven", Score: 7}, - }), - expectedError: nil, - }, - { - name: "2. Get elements within score range with score.", - presetValues: map[string]interface{}{ - "ZrangeStoreKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - destination: "ZrangeStoreDestinationKey2", - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey2", "ZrangeStoreKey2", "3", "7", "BYSCORE", "WITHSCORES"}, - expectedResponse: 5, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, - {Value: "six", Score: 6}, {Value: "seven", Score: 7}, - }), - expectedError: nil, - }, - { - // 3. Get elements within score range with offset and limit. - // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - name: "3. Get elements within score range with offset and limit.", - presetValues: map[string]interface{}{ - "ZrangeStoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - destination: "ZrangeStoreDestinationKey3", - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey3", "ZrangeStoreKey3", "3", "7", "BYSCORE", "WITHSCORES", "LIMIT", "2", "4"}, - expectedResponse: 3, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, {Value: "four", Score: 4}, {Value: "five", Score: 5}, - }), - expectedError: nil, - }, - { - // 4. Get elements within score range with offset and limit + reverse the results. - // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - // REV reverses the original set before getting the range. - name: "4. Get elements within score range with offset and limit + reverse the results.", - presetValues: map[string]interface{}{ - "ZrangeStoreKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - destination: "ZrangeStoreDestinationKey4", - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey4", "ZrangeStoreKey4", "3", "7", "BYSCORE", "WITHSCORES", "LIMIT", "2", "4", "REV"}, - expectedResponse: 3, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "six", Score: 6}, {Value: "five", Score: 5}, {Value: "four", Score: 4}, - }), - expectedError: nil, - }, - { - name: "5. Get elements within lex range without score.", - presetValues: map[string]interface{}{ - "ZrangeStoreKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "e", Score: 1}, - {Value: "b", Score: 1}, {Value: "f", Score: 1}, - {Value: "c", Score: 1}, {Value: "g", Score: 1}, - {Value: "d", Score: 1}, {Value: "h", Score: 1}, - }), - }, - destination: "ZrangeStoreDestinationKey5", - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey5", "ZrangeStoreKey5", "c", "g", "BYLEX"}, - expectedResponse: 5, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "c", Score: 1}, {Value: "d", Score: 1}, {Value: "e", Score: 1}, - {Value: "f", Score: 1}, {Value: "g", Score: 1}, - }), - expectedError: nil, - }, - { - name: "6. Get elements within lex range with score.", - presetValues: map[string]interface{}{ - "ZrangeStoreKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "e", Score: 1}, - {Value: "b", Score: 1}, {Value: "f", Score: 1}, - {Value: "c", Score: 1}, {Value: "g", Score: 1}, - {Value: "d", Score: 1}, {Value: "h", Score: 1}, - }), - }, - destination: "ZrangeStoreDestinationKey6", - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey6", "ZrangeStoreKey6", "a", "f", "BYLEX", "WITHSCORES"}, - expectedResponse: 6, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "b", Score: 1}, {Value: "c", Score: 1}, - {Value: "d", Score: 1}, {Value: "e", Score: 1}, {Value: "f", Score: 1}, - }), - expectedError: nil, - }, - { - // 7. Get elements within lex range with offset and limit. - // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - name: "7. Get elements within lex range with offset and limit.", - presetValues: map[string]interface{}{ - "ZrangeStoreKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "b", Score: 1}, - {Value: "c", Score: 1}, {Value: "d", Score: 1}, - {Value: "e", Score: 1}, {Value: "f", Score: 1}, - {Value: "g", Score: 1}, {Value: "h", Score: 1}, - }), - }, - destination: "ZrangeStoreDestinationKey7", - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey7", "ZrangeStoreKey7", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, - expectedResponse: 3, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "c", Score: 1}, {Value: "d", Score: 1}, {Value: "e", Score: 1}, - }), - expectedError: nil, - }, - { - // 8. Get elements within lex range with offset and limit + reverse the results. - // Offset and limit are in where we start and stop counting in the original sorted set (NOT THE RESULT). - // REV reverses the original set before getting the range. - name: "8. Get elements within lex range with offset and limit + reverse the results.", - presetValues: map[string]interface{}{ - "ZrangeStoreKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "b", Score: 1}, - {Value: "c", Score: 1}, {Value: "d", Score: 1}, - {Value: "e", Score: 1}, {Value: "f", Score: 1}, - {Value: "g", Score: 1}, {Value: "h", Score: 1}, - }), - }, - destination: "ZrangeStoreDestinationKey8", - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey8", "ZrangeStoreKey8", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4", "REV"}, - expectedResponse: 3, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "f", Score: 1}, {Value: "e", Score: 1}, {Value: "d", Score: 1}, - }), - expectedError: nil, - }, - { - name: "9. Return an empty slice when we use BYLEX while elements have different scores", - presetValues: map[string]interface{}{ - "ZrangeStoreKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "a", Score: 1}, {Value: "b", Score: 5}, - {Value: "c", Score: 2}, {Value: "d", Score: 6}, - {Value: "e", Score: 3}, {Value: "f", Score: 7}, - {Value: "g", Score: 4}, {Value: "h", Score: 8}, - }), - }, - destination: "ZrangeStoreDestinationKey9", - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey9", "ZrangeStoreKey9", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, - expectedResponse: 0, - expectedValue: nil, - expectedError: nil, - }, - { - name: "10. Throw error when limit does not provide both offset and limit", - presetValues: nil, - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey10", "ZrangeStoreKey10", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2"}, - expectedResponse: 0, - expectedError: errors.New("limit should contain offset and count as integers"), - }, - { - name: "11. Throw error when offset is not a valid integer", - presetValues: nil, - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey11", "ZrangeStoreKey11", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "offset", "4"}, - expectedResponse: 0, - expectedError: errors.New("limit offset must be integer"), - }, - { - name: "12. Throw error when limit is not a valid integer", - presetValues: nil, - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey12", "ZrangeStoreKey12", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "4", "limit"}, - expectedResponse: 0, - expectedError: errors.New("limit count must be integer"), - }, - { - name: "13. Throw error when offset is negative", - presetValues: nil, - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey13", "ZrangeStoreKey13", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9"}, - expectedResponse: 0, - expectedError: errors.New("limit offset must be >= 0"), - }, - { - name: "14. Throw error when the key does not hold a sorted set", - presetValues: map[string]interface{}{ - "ZrangeStoreKey14": "Default value", - }, - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey14", "ZrangeStoreKey14", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "2", "4"}, - expectedResponse: 0, - expectedError: errors.New("value at ZrangeStoreKey14 is not a sorted set"), - }, - { - name: "15. Command too short", - presetValues: nil, - command: []string{"ZRANGESTORE", "ZrangeStoreKey15", "1"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "16 Command too long", - presetValues: nil, - command: []string{"ZRANGESTORE", "ZrangeStoreDestinationKey16", "ZrangeStoreKey16", "a", "h", "BYLEX", "WITHSCORES", "LIMIT", "-4", "9", "REV", "WITHSCORES"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), - } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) - } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) + if test.expectedValue.Get(value).Score != score { + t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) } } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - - // Check if the resulting sorted set has the expected members/scores - if test.expectedValue == nil { - return - } - - if err = client.WriteArray([]resp.Value{ - resp.StringValue("ZRANGE"), - resp.StringValue(test.destination), - resp.StringValue("-inf"), - resp.StringValue("+inf"), - resp.StringValue("BYSCORE"), - resp.StringValue("WITHSCORES"), - }); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != test.expectedValue.Cardinality() { - t.Errorf("expected resulting set %s to have cardinality %d, got %d", - test.destination, test.expectedValue.Cardinality(), len(res.Array())) - } - - for _, member := range res.Array() { - value := sorted_set.Value(member.Array()[0].String()) - score := sorted_set.Score(member.Array()[1].Float()) - if !test.expectedValue.Contains(value) { - t.Errorf("unexpected value %s in resulting sorted set", value) - } - if test.expectedValue.Get(value).Score != score { - t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) - } - } - }) - } -} - -func Test_HandleZINTER(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedResponse [][]string - expectedError error - }{ - { - name: "1. Get the intersection between 2 sorted sets.", - presetValues: map[string]interface{}{ - "ZinterKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - "ZinterKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - command: []string{"ZINTER", "ZinterKey1", "ZinterKey2"}, - expectedResponse: [][]string{{"three"}, {"four"}, {"five"}}, - expectedError: nil, - }, - { - // 2. Get the intersection between 3 sorted sets with scores. - // By default, the SUM aggregate will be used. - name: "2. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 8}, - }), - "ZinterKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZINTER", "ZinterKey3", "ZinterKey4", "ZinterKey5", "WITHSCORES"}, - expectedResponse: [][]string{{"one", "3"}, {"eight", "24"}}, - expectedError: nil, - }, - { - // 3. Get the intersection between 3 sorted sets with scores. - // Use MIN aggregate. - name: "3. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZinterKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZINTER", "ZinterKey6", "ZinterKey7", "ZinterKey8", "WITHSCORES", "AGGREGATE", "MIN"}, - expectedResponse: [][]string{{"one", "1"}, {"eight", "8"}}, - expectedError: nil, - }, - { - // 4. Get the intersection between 3 sorted sets with scores. - // Use MAX aggregate. - name: "4. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZinterKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZINTER", "ZinterKey9", "ZinterKey10", "ZinterKey11", "WITHSCORES", "AGGREGATE", "MAX"}, - expectedResponse: [][]string{{"one", "1000"}, {"eight", "800"}}, - expectedError: nil, - }, - { - // 5. Get the intersection between 3 sorted sets with scores. - // Use SUM aggregate with weights modifier. - name: "5. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZinterKey14": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZINTER", "ZinterKey12", "ZinterKey13", "ZinterKey14", "WITHSCORES", "AGGREGATE", "SUM", "WEIGHTS", "1", "5", "3"}, - expectedResponse: [][]string{{"one", "3105"}, {"eight", "2808"}}, - expectedError: nil, - }, - { - // 6. Get the intersection between 3 sorted sets with scores. - // Use MAX aggregate with added weights. - name: "6. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterKey15": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterKey16": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZinterKey17": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZINTER", "ZinterKey15", "ZinterKey16", "ZinterKey17", "WITHSCORES", "AGGREGATE", "MAX", "WEIGHTS", "1", "5", "3"}, - expectedResponse: [][]string{{"one", "3000"}, {"eight", "2400"}}, - expectedError: nil, - }, - { - // 7. Get the intersection between 3 sorted sets with scores. - // Use MIN aggregate with added weights. - name: "7. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterKey18": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterKey19": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZinterKey20": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZINTER", "ZinterKey18", "ZinterKey19", "ZinterKey20", "WITHSCORES", "AGGREGATE", "MIN", "WEIGHTS", "1", "5", "3"}, - expectedResponse: [][]string{{"one", "5"}, {"eight", "8"}}, - expectedError: nil, - }, - { - name: "8. Throw an error if there are more weights than keys", - presetValues: map[string]interface{}{ - "ZinterKey21": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterKey22": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZINTER", "ZinterKey21", "ZinterKey22", "WEIGHTS", "1", "2", "3"}, - expectedResponse: nil, - expectedError: errors.New("number of weights should match number of keys"), - }, - { - name: "9. Throw an error if there are fewer weights than keys", - presetValues: map[string]interface{}{ - "ZinterKey23": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterKey24": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - }), - "ZinterKey25": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZINTER", "ZinterKey23", "ZinterKey24", "ZinterKey25", "WEIGHTS", "5", "4"}, - expectedResponse: nil, - expectedError: errors.New("number of weights should match number of keys"), - }, - { - name: "10. Throw an error if there are no keys provided", - presetValues: map[string]interface{}{ - "ZinterKey26": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - "ZinterKey27": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - "ZinterKey28": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZINTER", "WEIGHTS", "5", "4"}, - expectedResponse: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "11. Throw an error if any of the provided keys are not sorted sets", - presetValues: map[string]interface{}{ - "ZinterKey29": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterKey30": "Default value", - "ZinterKey31": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZINTER", "ZinterKey29", "ZinterKey30", "ZinterKey31"}, - expectedResponse: nil, - expectedError: errors.New("value at ZinterKey30 is not a sorted set"), - }, - { - name: "12. If any of the keys does not exist, return an empty array.", - presetValues: map[string]interface{}{ - "ZinterKey32": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, - }), - "ZinterKey33": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZINTER", "non-existent", "ZinterKey32", "ZinterKey33"}, - expectedResponse: [][]string{}, - expectedError: nil, - }, - { - name: "13. Command too short", - command: []string{"ZINTER"}, - expectedResponse: [][]string{}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), - } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) - } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) - } - return - } - - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) - } - - for _, item := range res.Array() { - value := item.Array()[0].String() - score := func() string { - if len(item.Array()) == 2 { - return item.Array()[1].String() - } - return "" - }() - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - return expected[0] == value - }) { - t.Errorf("unexpected member \"%s\" in response", value) - } - if score != "" { - for _, expected := range test.expectedResponse { - if expected[0] == value && expected[1] != score { - t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) - } - } - } - } - }) - } -} - -func Test_HandleZINTERSTORE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - destination string - command []string - expectedValue *sorted_set.SortedSet - expectedResponse int - expectedError error - }{ - { - name: "1. Get the intersection between 2 sorted sets.", - presetValues: map[string]interface{}{ - "ZinterStoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - "ZinterStoreKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - destination: "ZinterStoreDestinationKey1", - command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey1", "ZinterStoreKey1", "ZinterStoreKey2"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 6}, {Value: "four", Score: 8}, - {Value: "five", Score: 10}, - }), - expectedResponse: 3, - expectedError: nil, - }, - { - // 2. Get the intersection between 3 sorted sets with scores. - // By default, the SUM aggregate will be used. - name: "2. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterStoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterStoreKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 8}, - }), - "ZinterStoreKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZinterStoreDestinationKey2", - command: []string{ - "ZINTERSTORE", "ZinterStoreDestinationKey2", "ZinterStoreKey3", "ZinterStoreKey4", "ZinterStoreKey5", "WITHSCORES", - }, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 3}, {Value: "eight", Score: 24}, - }), - expectedResponse: 2, - expectedError: nil, - }, - { - // 3. Get the intersection between 3 sorted sets with scores. - // Use MIN aggregate. - name: "3. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterStoreKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterStoreKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZinterStoreKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZinterStoreDestinationKey3", - command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey3", "ZinterStoreKey6", "ZinterStoreKey7", "ZinterStoreKey8", "WITHSCORES", "AGGREGATE", "MIN"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "eight", Score: 8}, - }), - expectedResponse: 2, - expectedError: nil, - }, - { - // 4. Get the intersection between 3 sorted sets with scores. - // Use MAX aggregate. - name: "4. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterStoreKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterStoreKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZinterStoreKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZinterStoreDestinationKey4", - command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey4", "ZinterStoreKey9", "ZinterStoreKey10", "ZinterStoreKey11", "WITHSCORES", "AGGREGATE", "MAX"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - }), - expectedResponse: 2, - expectedError: nil, - }, - { - // 5. Get the intersection between 3 sorted sets with scores. - // Use SUM aggregate with weights modifier. - name: "5. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterStoreKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterStoreKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZinterStoreKey14": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZinterStoreDestinationKey5", - command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey5", "ZinterStoreKey12", "ZinterStoreKey13", "ZinterStoreKey14", "WITHSCORES", "AGGREGATE", "SUM", "WEIGHTS", "1", "5", "3"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 3105}, {Value: "eight", Score: 2808}, - }), - expectedResponse: 2, - expectedError: nil, - }, - { - // 6. Get the intersection between 3 sorted sets with scores. - // Use MAX aggregate with added weights. - name: "6. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterStoreKey15": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterStoreKey16": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZinterStoreKey17": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZinterStoreDestinationKey6", - command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey6", "ZinterStoreKey15", "ZinterStoreKey16", "ZinterStoreKey17", "WITHSCORES", "AGGREGATE", "MAX", "WEIGHTS", "1", "5", "3"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 3000}, {Value: "eight", Score: 2400}, - }), - expectedResponse: 2, - expectedError: nil, - }, - { - // 7. Get the intersection between 3 sorted sets with scores. - // Use MIN aggregate with added weights. - name: "7. Get the intersection between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZinterStoreKey18": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterStoreKey19": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZinterStoreKey20": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZinterStoreDestinationKey7", - command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey7", "ZinterStoreKey18", "ZinterStoreKey19", "ZinterStoreKey20", "WITHSCORES", "AGGREGATE", "MIN", "WEIGHTS", "1", "5", "3"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 5}, {Value: "eight", Score: 8}, - }), - expectedResponse: 2, - expectedError: nil, - }, - { - name: "8. Throw an error if there are more weights than keys", - presetValues: map[string]interface{}{ - "ZinterStoreKey21": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterStoreKey22": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey8", "ZinterStoreKey21", "ZinterStoreKey22", "WEIGHTS", "1", "2", "3"}, - expectedResponse: 0, - expectedError: errors.New("number of weights should match number of keys"), - }, - { - name: "9. Throw an error if there are fewer weights than keys", - presetValues: map[string]interface{}{ - "ZinterStoreKey23": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterStoreKey24": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - }), - "ZinterStoreKey25": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey9", "ZinterStoreKey23", "ZinterStoreKey24", "ZinterStoreKey25", "WEIGHTS", "5", "4"}, - expectedResponse: 0, - expectedError: errors.New("number of weights should match number of keys"), - }, - { - name: "10. Throw an error if there are no keys provided", - presetValues: map[string]interface{}{ - "ZinterStoreKey26": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - "ZinterStoreKey27": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - "ZinterStoreKey28": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZINTERSTORE", "WEIGHTS", "5", "4"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "11. Throw an error if any of the provided keys are not sorted sets", - presetValues: map[string]interface{}{ - "ZinterStoreKey29": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZinterStoreKey30": "Default value", - "ZinterStoreKey31": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZINTERSTORE", "ZinterStoreKey29", "ZinterStoreKey30", "ZinterStoreKey31"}, - expectedResponse: 0, - expectedError: errors.New("value at ZinterStoreKey30 is not a sorted set"), - }, - { - name: "12. If any of the keys does not exist, return an empty array.", - presetValues: map[string]interface{}{ - "ZinterStoreKey32": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, - }), - "ZinterStoreKey33": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZINTERSTORE", "ZinterStoreDestinationKey12", "non-existent", "ZinterStoreKey32", "ZinterStoreKey33"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "13. Command too short", - command: []string{"ZINTERSTORE"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), - } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) - } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - - // Check if the resulting sorted set has the expected members/scores - if test.expectedValue == nil { - return - } - - if err = client.WriteArray([]resp.Value{ - resp.StringValue("ZRANGE"), - resp.StringValue(test.destination), - resp.StringValue("-inf"), - resp.StringValue("+inf"), - resp.StringValue("BYSCORE"), - resp.StringValue("WITHSCORES"), - }); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != test.expectedValue.Cardinality() { - t.Errorf("expected resulting set %s to have cardinality %d, got %d", - test.destination, test.expectedValue.Cardinality(), len(res.Array())) - } - - for _, member := range res.Array() { - value := sorted_set.Value(member.Array()[0].String()) - score := sorted_set.Score(member.Array()[1].Float()) - if !test.expectedValue.Contains(value) { - t.Errorf("unexpected value %s in resulting sorted set", value) - } - if test.expectedValue.Get(value).Score != score { - t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) - } - } - }) - } -} - -func Test_HandleZUNION(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - presetValues map[string]interface{} - command []string - expectedResponse [][]string - expectedError error - }{ - { - name: "1. Get the union between 2 sorted sets.", - presetValues: map[string]interface{}{ - "ZunionKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - "ZunionKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - command: []string{"ZUNION", "ZunionKey1", "ZunionKey2"}, - expectedResponse: [][]string{{"one"}, {"two"}, {"three"}, {"four"}, {"five"}, {"six"}, {"seven"}, {"eight"}}, - expectedError: nil, - }, - { - // 2. Get the union between 3 sorted sets with scores. - // By default, the SUM aggregate will be used. - name: "2. Get the union between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZunionKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 8}, - }), - "ZunionKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 36}, - }), - }, - command: []string{"ZUNION", "ZunionKey3", "ZunionKey4", "ZunionKey5", "WITHSCORES"}, - expectedResponse: [][]string{ - {"one", "3"}, {"two", "4"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, - {"seven", "7"}, {"eight", "24"}, {"nine", "9"}, {"ten", "10"}, {"eleven", "11"}, - {"twelve", "24"}, {"thirty-six", "72"}, - }, - expectedError: nil, - }, - { - // 3. Get the union between 3 sorted sets with scores. - // Use MIN aggregate. - name: "3. Get the union between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZunionKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZunionKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 72}, - }), - }, - command: []string{"ZUNION", "ZunionKey6", "ZunionKey7", "ZunionKey8", "WITHSCORES", "AGGREGATE", "MIN"}, - expectedResponse: [][]string{ - {"one", "1"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, - {"seven", "7"}, {"eight", "8"}, {"nine", "9"}, {"ten", "10"}, {"eleven", "11"}, - {"twelve", "12"}, {"thirty-six", "36"}, - }, - expectedError: nil, - }, - { - // 4. Get the union between 3 sorted sets with scores. - // Use MAX aggregate. - name: "4. Get the union between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZunionKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZunionKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 72}, - }), - }, - command: []string{"ZUNION", "ZunionKey9", "ZunionKey10", "ZunionKey11", "WITHSCORES", "AGGREGATE", "MAX"}, - expectedResponse: [][]string{ - {"one", "1000"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, - {"seven", "7"}, {"eight", "800"}, {"nine", "9"}, {"ten", "10"}, {"eleven", "11"}, - {"twelve", "12"}, {"thirty-six", "72"}, - }, - expectedError: nil, - }, - { - // 5. Get the union between 3 sorted sets with scores. - // Use SUM aggregate with weights modifier. - name: "5. Get the union between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZunionKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZunionKey14": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZUNION", "ZunionKey12", "ZunionKey13", "ZunionKey14", "WITHSCORES", "AGGREGATE", "SUM", "WEIGHTS", "1", "2", "3"}, - expectedResponse: [][]string{ - {"one", "3102"}, {"two", "6"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, - {"seven", "7"}, {"eight", "2568"}, {"nine", "27"}, {"ten", "30"}, {"eleven", "22"}, - {"twelve", "60"}, {"thirty-six", "72"}, - }, - expectedError: nil, - }, - { - // 6. Get the union between 3 sorted sets with scores. - // Use MAX aggregate with added weights. - name: "6. Get the union between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZunionKey15": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionKey16": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZunionKey17": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZUNION", "ZunionKey15", "ZunionKey16", "ZunionKey17", "WITHSCORES", "AGGREGATE", "MAX", "WEIGHTS", "1", "2", "3"}, - expectedResponse: [][]string{ - {"one", "3000"}, {"two", "4"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, - {"seven", "7"}, {"eight", "2400"}, {"nine", "27"}, {"ten", "30"}, {"eleven", "22"}, - {"twelve", "36"}, {"thirty-six", "72"}, - }, - expectedError: nil, - }, - { - // 7. Get the union between 3 sorted sets with scores. - // Use MIN aggregate with added weights. - name: "7. Get the union between 3 sorted sets with scores.", - presetValues: map[string]interface{}{ - "ZunionKey18": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionKey19": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZunionKey20": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZUNION", "ZunionKey18", "ZunionKey19", "ZunionKey20", "WITHSCORES", "AGGREGATE", "MIN", "WEIGHTS", "1", "2", "3"}, - expectedResponse: [][]string{ - {"one", "2"}, {"two", "2"}, {"three", "3"}, {"four", "4"}, {"five", "5"}, {"six", "6"}, {"seven", "7"}, - {"eight", "8"}, {"nine", "27"}, {"ten", "30"}, {"eleven", "22"}, {"twelve", "24"}, {"thirty-six", "72"}, - }, - expectedError: nil, - }, - { - name: "8. Throw an error if there are more weights than keys", - presetValues: map[string]interface{}{ - "ZunionKey21": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionKey22": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZUNION", "ZunionKey21", "ZunionKey22", "WEIGHTS", "1", "2", "3"}, - expectedResponse: nil, - expectedError: errors.New("number of weights should match number of keys"), - }, - { - name: "9. Throw an error if there are fewer weights than keys", - presetValues: map[string]interface{}{ - "ZunionKey23": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionKey24": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - }), - "ZunionKey25": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZUNION", "ZunionKey23", "ZunionKey24", "ZunionKey25", "WEIGHTS", "5", "4"}, - expectedResponse: nil, - expectedError: errors.New("number of weights should match number of keys"), - }, - { - name: "10. Throw an error if there are no keys provided", - presetValues: map[string]interface{}{ - "ZunionKey26": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - "ZunionKey27": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - "ZunionKey28": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZUNION", "WEIGHTS", "5", "4"}, - expectedResponse: nil, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "11. Throw an error if any of the provided keys are not sorted sets", - presetValues: map[string]interface{}{ - "ZunionKey29": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionKey30": "Default value", - "ZunionKey31": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZUNION", "ZunionKey29", "ZunionKey30", "ZunionKey31"}, - expectedResponse: nil, - expectedError: errors.New("value at ZunionKey30 is not a sorted set"), - }, - { - name: "12. If any of the keys does not exist, skip it.", - presetValues: map[string]interface{}{ - "ZunionKey32": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, - }), - "ZunionKey33": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - command: []string{"ZUNION", "non-existent", "ZunionKey32", "ZunionKey33"}, - expectedResponse: [][]string{ - {"one"}, {"two"}, {"thirty-six"}, {"twelve"}, {"eleven"}, - {"seven"}, {"eight"}, {"nine"}, {"ten"}, - }, - expectedError: nil, - }, - { - name: "13. Command too short", - command: []string{"ZUNION"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), - } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) - } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) - } - return - } - - if len(res.Array()) != len(test.expectedResponse) { - t.Errorf("expected response array of length %d, got %d", len(test.expectedResponse), len(res.Array())) - } - - for _, item := range res.Array() { - value := item.Array()[0].String() - score := func() string { - if len(item.Array()) == 2 { - return item.Array()[1].String() - } - return "" - }() - if !slices.ContainsFunc(test.expectedResponse, func(expected []string) bool { - return expected[0] == value - }) { - t.Errorf("unexpected member \"%s\" in response", value) - } - if score != "" { - for _, expected := range test.expectedResponse { - if expected[0] == value && expected[1] != score { - t.Errorf("expected score for member \"%s\" to be %s, got %s", value, expected[1], score) - } - } - } - } - }) - } -} - -func Test_HandleZUNIONSTORE(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error() - } - client := resp.NewConn(conn) - - tests := []struct { - name string - preset bool - presetValues map[string]interface{} - destination string - command []string - expectedValue *sorted_set.SortedSet - expectedResponse int - expectedError error - }{ - { - name: "1. Get the union between 2 sorted sets.", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey1": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, - }), - "ZunionStoreKey2": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - }, - destination: "ZunionStoreDestinationKey1", - command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey1", "ZunionStoreKey1", "ZunionStoreKey2"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 6}, {Value: "four", Score: 8}, - {Value: "five", Score: 10}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - expectedResponse: 8, - expectedError: nil, - }, - { - // 2. Get the union between 3 sorted sets with scores. - // By default, the SUM aggregate will be used. - name: "2. Get the union between 3 sorted sets with scores.", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey3": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionStoreKey4": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 8}, - }), - "ZunionStoreKey5": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 36}, - }), - }, - destination: "ZunionStoreDestinationKey2", - command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey2", "ZunionStoreKey3", "ZunionStoreKey4", "ZunionStoreKey5", "WITHSCORES"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 3}, {Value: "two", Score: 4}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 24}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "eleven", Score: 11}, - {Value: "twelve", Score: 24}, {Value: "thirty-six", Score: 72}, - }), - expectedResponse: 13, - expectedError: nil, - }, - { - // 3. Get the union between 3 sorted sets with scores. - // Use MIN aggregate. - name: "3. Get the union between 3 sorted sets with scores.", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey6": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionStoreKey7": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZunionStoreKey8": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 72}, - }), - }, - destination: "ZunionStoreDestinationKey3", - command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey3", "ZunionStoreKey6", "ZunionStoreKey7", "ZunionStoreKey8", "WITHSCORES", "AGGREGATE", "MIN"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "eleven", Score: 11}, - {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 36}, - }), - expectedResponse: 13, - expectedError: nil, - }, - { - // 4. Get the union between 3 sorted sets with scores. - // Use MAX aggregate. - name: "4. Get the union between 3 sorted sets with scores.", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey9": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionStoreKey10": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZunionStoreKey11": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 72}, - }), - }, - destination: "ZunionStoreDestinationKey4", - command: []string{ - "ZUNIONSTORE", "ZunionStoreDestinationKey4", "ZunionStoreKey9", "ZunionStoreKey10", "ZunionStoreKey11", "WITHSCORES", "AGGREGATE", "MAX", - }, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "eleven", Score: 11}, - {Value: "twelve", Score: 12}, {Value: "thirty-six", Score: 72}, - }), - expectedResponse: 13, - expectedError: nil, - }, - { - // 5. Get the union between 3 sorted sets with scores. - // Use SUM aggregate with weights modifier. - name: "5. Get the union between 3 sorted sets with scores.", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey12": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionStoreKey13": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZunionStoreKey14": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZunionStoreDestinationKey5", - command: []string{ - "ZUNIONSTORE", "ZunionStoreDestinationKey5", "ZunionStoreKey12", "ZunionStoreKey13", "ZunionStoreKey14", - "WITHSCORES", "AGGREGATE", "SUM", "WEIGHTS", "1", "2", "3", - }, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 3102}, {Value: "two", Score: 6}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 2568}, - {Value: "nine", Score: 27}, {Value: "ten", Score: 30}, {Value: "eleven", Score: 22}, - {Value: "twelve", Score: 60}, {Value: "thirty-six", Score: 72}, - }), - expectedResponse: 13, - expectedError: nil, - }, - { - // 6. Get the union between 3 sorted sets with scores. - // Use MAX aggregate with added weights. - name: "6. Get the union between 3 sorted sets with scores.", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey15": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionStoreKey16": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZunionStoreKey17": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZunionStoreDestinationKey6", - command: []string{ - "ZUNIONSTORE", "ZunionStoreDestinationKey6", "ZunionStoreKey15", "ZunionStoreKey16", "ZunionStoreKey17", - "WITHSCORES", "AGGREGATE", "MAX", "WEIGHTS", "1", "2", "3"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 3000}, {Value: "two", Score: 4}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 2400}, - {Value: "nine", Score: 27}, {Value: "ten", Score: 30}, {Value: "eleven", Score: 22}, - {Value: "twelve", Score: 36}, {Value: "thirty-six", Score: 72}, - }), - expectedResponse: 13, - expectedError: nil, - }, - { - // 7. Get the union between 3 sorted sets with scores. - // Use MIN aggregate with added weights. - name: "7. Get the union between 3 sorted sets with scores.", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey18": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 100}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionStoreKey19": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, {Value: "eight", Score: 80}, - }), - "ZunionStoreKey20": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1000}, {Value: "eight", Score: 800}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZunionStoreDestinationKey7", - command: []string{ - "ZUNIONSTORE", "ZunionStoreDestinationKey7", "ZunionStoreKey18", "ZunionStoreKey19", "ZunionStoreKey20", - "WITHSCORES", "AGGREGATE", "MIN", "WEIGHTS", "1", "2", "3", - }, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 2}, {Value: "two", Score: 2}, {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 27}, {Value: "ten", Score: 30}, {Value: "eleven", Score: 22}, - {Value: "twelve", Score: 24}, {Value: "thirty-six", Score: 72}, - }), - expectedResponse: 13, - expectedError: nil, - }, - { - name: "8. Throw an error if there are more weights than keys", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey21": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionStoreKey22": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - destination: "ZunionStoreDestinationKey8", - command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey8", "ZunionStoreKey21", "ZunionStoreKey22", "WEIGHTS", "1", "2", "3"}, - expectedResponse: 0, - expectedError: errors.New("number of weights should match number of keys"), - }, - { - name: "9. Throw an error if there are fewer weights than keys", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey23": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionStoreKey24": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - }), - "ZunionStoreKey25": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - destination: "ZunionStoreDestinationKey9", - command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey9", "ZunionStoreKey23", "ZunionStoreKey24", "ZunionStoreKey25", "WEIGHTS", "5", "4"}, - expectedResponse: 0, - expectedError: errors.New("number of weights should match number of keys"), - }, - { - name: "10. Throw an error if there are no keys provided", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey26": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - "ZunionStoreKey27": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - "ZunionStoreKey28": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - command: []string{"ZUNIONSTORE", "WEIGHTS", "5", "4"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "11. Throw an error if any of the provided keys are not sorted sets", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey29": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "three", Score: 3}, {Value: "four", Score: 4}, - {Value: "five", Score: 5}, {Value: "six", Score: 6}, - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - }), - "ZunionStoreKey30": "Default value", - "ZunionStoreKey31": sorted_set.NewSortedSet([]sorted_set.MemberParam{{Value: "one", Score: 1}}), - }, - destination: "ZunionStoreDestinationKey11", - command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey11", "ZunionStoreKey29", "ZunionStoreKey30", "ZunionStoreKey31"}, - expectedResponse: 0, - expectedError: errors.New("value at ZunionStoreKey30 is not a sorted set"), - }, - { - name: "12. If any of the keys does not exist, skip it.", - preset: true, - presetValues: map[string]interface{}{ - "ZunionStoreKey32": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, - {Value: "thirty-six", Score: 36}, {Value: "twelve", Score: 12}, - {Value: "eleven", Score: 11}, - }), - "ZunionStoreKey33": sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, - {Value: "twelve", Score: 12}, - }), - }, - destination: "ZunionStoreDestinationKey12", - command: []string{"ZUNIONSTORE", "ZunionStoreDestinationKey12", "non-existent", "ZunionStoreKey32", "ZunionStoreKey33"}, - expectedValue: sorted_set.NewSortedSet([]sorted_set.MemberParam{ - {Value: "one", Score: 1}, {Value: "two", Score: 2}, {Value: "seven", Score: 7}, {Value: "eight", Score: 8}, - {Value: "nine", Score: 9}, {Value: "ten", Score: 10}, {Value: "eleven", Score: 11}, {Value: "twelve", Score: 24}, - {Value: "thirty-six", Score: 36}, - }), - expectedResponse: 9, - expectedError: nil, - }, - { - name: "13. Command too short", - preset: false, - command: []string{"ZUNIONSTORE"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValues != nil { - var command []resp.Value - var expected string - for key, value := range test.presetValues { - switch value.(type) { - case string: - command = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(value.(string)), - } - expected = "ok" - case *sorted_set.SortedSet: - command = []resp.Value{resp.StringValue("ZADD"), resp.StringValue(key)} - for _, member := range value.(*sorted_set.SortedSet).GetAll() { - command = append(command, []resp.Value{ - resp.StringValue(strconv.FormatFloat(float64(member.Score), 'f', -1, 64)), - resp.StringValue(string(member.Value)), - }...) - } - expected = strconv.Itoa(value.(*sorted_set.SortedSet).Cardinality()) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if !strings.EqualFold(res.String(), expected) { - t.Errorf("expected preset response to be \"%s\", got %s", expected, res.String()) - } - } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), res.Error().Error()) - } - return - } - - if res.Integer() != test.expectedResponse { - t.Errorf("expected response %d, got %d", test.expectedResponse, res.Integer()) - } - - // Check if the resulting sorted set has the expected members/scores - if test.expectedValue == nil { - return - } - - if err = client.WriteArray([]resp.Value{ - resp.StringValue("ZRANGE"), - resp.StringValue(test.destination), - resp.StringValue("-inf"), - resp.StringValue("+inf"), - resp.StringValue("BYSCORE"), - resp.StringValue("WITHSCORES"), - }); err != nil { - t.Error(err) - } - - res, _, err = client.ReadValue() - if err != nil { - t.Error(err) - } - - if len(res.Array()) != test.expectedValue.Cardinality() { - t.Errorf("expected resulting set %s to have cardinality %d, got %d", - test.destination, test.expectedValue.Cardinality(), len(res.Array())) - } - - for _, member := range res.Array() { - value := sorted_set.Value(member.Array()[0].String()) - score := sorted_set.Score(member.Array()[1].Float()) - if !test.expectedValue.Contains(value) { - t.Errorf("unexpected value %s in resulting sorted set", value) - } - if test.expectedValue.Get(value).Score != score { - t.Errorf("expected value %s to have score %v, got %v", value, test.expectedValue.Get(value).Score, score) - } - } - }) - } + }) + } + }) } diff --git a/internal/modules/string/commands_test.go b/internal/modules/string/commands_test.go index 6acd90ee..c92e419e 100644 --- a/internal/modules/string/commands_test.go +++ b/internal/modules/string/commands_test.go @@ -16,148 +16,173 @@ package str_test import ( "errors" - "fmt" "github.com/echovault/echovault/echovault" "github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/constants" "github.com/tidwall/resp" - "net" "strconv" "strings" - "sync" "testing" ) -var mockServer *echovault.EchoVault -var addr = "localhost" -var port int +func Test_String(t *testing.T) { + port, err := internal.GetFreePort() + if err != nil { + t.Error() + return + } -func init() { - port, _ = internal.GetFreePort() - mockServer, _ = echovault.NewEchoVault( + mockServer, err := echovault.NewEchoVault( echovault.WithConfig(config.Config{ - BindAddr: addr, + BindAddr: "localhost", Port: uint16(port), DataDir: "", EvictionPolicy: constants.NoEviction, }), ) - wg := sync.WaitGroup{} - wg.Add(1) + if err != nil { + t.Error(err) + return + } + go func() { - wg.Done() mockServer.Start() }() - wg.Wait() -} -func Test_HandleSetRange(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) + t.Cleanup(func() { + mockServer.ShutDown() + }) - tests := []struct { - name string - key string - presetValue string - command []string - expectedValue string - expectedResponse int - expectedError error - }{ - { - name: "Test that SETRANGE on non-existent string creates new string", - key: "SetRangeKey1", - presetValue: "", - command: []string{"SETRANGE", "SetRangeKey1", "10", "New String Value"}, - expectedValue: "New String Value", - expectedResponse: len("New String Value"), - expectedError: nil, - }, - { - name: "Test SETRANGE with an offset that leads to a longer resulting string", - key: "SetRangeKey2", - presetValue: "Original String Value", - command: []string{"SETRANGE", "SetRangeKey2", "16", "Portion Replaced With This New String"}, - expectedValue: "Original String Portion Replaced With This New String", - expectedResponse: len("Original String Portion Replaced With This New String"), - expectedError: nil, - }, - { - name: "SETRANGE with negative offset prepends the string", - key: "SetRangeKey3", - presetValue: "This is a preset value", - command: []string{"SETRANGE", "SetRangeKey3", "-10", "Prepended "}, - expectedValue: "Prepended This is a preset value", - expectedResponse: len("Prepended This is a preset value"), - expectedError: nil, - }, - { - name: "SETRANGE with offset that embeds new string inside the old string", - key: "SetRangeKey4", - presetValue: "This is a preset value", - command: []string{"SETRANGE", "SetRangeKey4", "0", "That"}, - expectedValue: "That is a preset value", - expectedResponse: len("That is a preset value"), - expectedError: nil, - }, - { - name: "SETRANGE with offset longer than original lengths appends the string", - key: "SetRangeKey5", - presetValue: "This is a preset value", - command: []string{"SETRANGE", "SetRangeKey5", "100", " Appended"}, - expectedValue: "This is a preset value Appended", - expectedResponse: len("This is a preset value Appended"), - expectedError: nil, - }, - { - name: "SETRANGE with offset on the last character replaces last character with new string", - key: "SetRangeKey6", - presetValue: "This is a preset value", - command: []string{"SETRANGE", "SetRangeKey6", strconv.Itoa(len("This is a preset value") - 1), " replaced"}, - expectedValue: "This is a preset valu replaced", - expectedResponse: len("This is a preset valu replaced"), - expectedError: nil, - }, - { - name: " Offset not integer", - command: []string{"SETRANGE", "key", "offset", "value"}, - expectedResponse: 0, - expectedError: errors.New("offset must be an integer"), - }, - { - name: "SETRANGE target is not a string", - key: "test-int", - presetValue: "10", - command: []string{"SETRANGE", "test-int", "10", "value"}, - expectedResponse: 0, - expectedError: errors.New("value at key test-int is not a string"), - }, - { - name: "Command too short", - command: []string{"SETRANGE", "key"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "Command too long", - command: []string{"SETRANGE", "key", "offset", "value", "value1"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + t.Run("Test_HandleSetRange", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != "" { - if err = client.WriteArray([]resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue), - }); err != nil { + tests := []struct { + name string + key string + presetValue string + command []string + expectedValue string + expectedResponse int + expectedError error + }{ + { + name: "Test that SETRANGE on non-existent string creates new string", + key: "SetRangeKey1", + presetValue: "", + command: []string{"SETRANGE", "SetRangeKey1", "10", "New String Value"}, + expectedValue: "New String Value", + expectedResponse: len("New String Value"), + expectedError: nil, + }, + { + name: "Test SETRANGE with an offset that leads to a longer resulting string", + key: "SetRangeKey2", + presetValue: "Original String Value", + command: []string{"SETRANGE", "SetRangeKey2", "16", "Portion Replaced With This New String"}, + expectedValue: "Original String Portion Replaced With This New String", + expectedResponse: len("Original String Portion Replaced With This New String"), + expectedError: nil, + }, + { + name: "SETRANGE with negative offset prepends the string", + key: "SetRangeKey3", + presetValue: "This is a preset value", + command: []string{"SETRANGE", "SetRangeKey3", "-10", "Prepended "}, + expectedValue: "Prepended This is a preset value", + expectedResponse: len("Prepended This is a preset value"), + expectedError: nil, + }, + { + name: "SETRANGE with offset that embeds new string inside the old string", + key: "SetRangeKey4", + presetValue: "This is a preset value", + command: []string{"SETRANGE", "SetRangeKey4", "0", "That"}, + expectedValue: "That is a preset value", + expectedResponse: len("That is a preset value"), + expectedError: nil, + }, + { + name: "SETRANGE with offset longer than original lengths appends the string", + key: "SetRangeKey5", + presetValue: "This is a preset value", + command: []string{"SETRANGE", "SetRangeKey5", "100", " Appended"}, + expectedValue: "This is a preset value Appended", + expectedResponse: len("This is a preset value Appended"), + expectedError: nil, + }, + { + name: "SETRANGE with offset on the last character replaces last character with new string", + key: "SetRangeKey6", + presetValue: "This is a preset value", + command: []string{"SETRANGE", "SetRangeKey6", strconv.Itoa(len("This is a preset value") - 1), " replaced"}, + expectedValue: "This is a preset valu replaced", + expectedResponse: len("This is a preset valu replaced"), + expectedError: nil, + }, + { + name: " Offset not integer", + command: []string{"SETRANGE", "key", "offset", "value"}, + expectedResponse: 0, + expectedError: errors.New("offset must be an integer"), + }, + { + name: "SETRANGE target is not a string", + key: "test-int", + presetValue: "10", + command: []string{"SETRANGE", "test-int", "10", "value"}, + expectedResponse: 0, + expectedError: errors.New("value at key test-int is not a string"), + }, + { + name: "Command too short", + command: []string{"SETRANGE", "key"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "Command too long", + command: []string{"SETRANGE", "key", "offset", "value", "value1"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != "" { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue), + }); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } + + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } + } + + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } + + if err = client.WriteArray(command); err != nil { t.Error(err) } res, _, err := client.ReadValue() @@ -165,95 +190,100 @@ func Test_HandleSetRange(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected preset response to be OK, got %s", res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + }) + } + }) - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + t.Run("Test_HandleStrLen", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } + tests := []struct { + name string + key string + presetValue string + command []string + expectedResponse int + expectedError error + }{ + { + name: "Return the correct string length for an existing string", + key: "StrLenKey1", + presetValue: "Test String", + command: []string{"STRLEN", "StrLenKey1"}, + expectedResponse: len("Test String"), + expectedError: nil, + }, + { + name: "If the string does not exist, return 0", + key: "StrLenKey2", + presetValue: "", + command: []string{"STRLEN", "StrLenKey2"}, + expectedResponse: 0, + expectedError: nil, + }, + { + name: "Too few args", + key: "StrLenKey3", + presetValue: "", + command: []string{"STRLEN"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "Too many args", + key: "StrLenKey4", + presetValue: "", + command: []string{"STRLEN", "StrLenKey4", "StrLenKey5"}, + expectedResponse: 0, + expectedError: errors.New(constants.WrongArgsResponse), + }, + } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - }) - } -} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != "" { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue), + }); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } -func Test_HandleStrLen(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } + } - tests := []struct { - name string - key string - presetValue string - command []string - expectedResponse int - expectedError error - }{ - { - name: "Return the correct string length for an existing string", - key: "StrLenKey1", - presetValue: "Test String", - command: []string{"STRLEN", "StrLenKey1"}, - expectedResponse: len("Test String"), - expectedError: nil, - }, - { - name: "If the string does not exist, return 0", - key: "StrLenKey2", - presetValue: "", - command: []string{"STRLEN", "StrLenKey2"}, - expectedResponse: 0, - expectedError: nil, - }, - { - name: "Too few args", - key: "StrLenKey3", - presetValue: "", - command: []string{"STRLEN"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "Too many args", - key: "StrLenKey4", - presetValue: "", - command: []string{"STRLEN", "StrLenKey4", "StrLenKey5"}, - expectedResponse: 0, - expectedError: errors.New(constants.WrongArgsResponse), - }, - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != "" { - if err = client.WriteArray([]resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue), - }); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } res, _, err := client.ReadValue() @@ -261,140 +291,145 @@ func Test_HandleStrLen(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected preset response to be OK, got %s", res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } + if res.Integer() != test.expectedResponse { + t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) + } + }) + } + }) - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } + t.Run("Test_HandleSubStr", func(t *testing.T) { + t.Parallel() + conn, err := internal.GetConnection("localhost", port) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = conn.Close() + }() + client := resp.NewConn(conn) - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) - } - return - } + tests := []struct { + name string + key string + presetValue string + command []string + expectedResponse string + expectedError error + }{ + { + name: "Return substring within the range of the string", + key: "SubStrKey1", + presetValue: "Test String One", + command: []string{"SUBSTR", "SubStrKey1", "5", "10"}, + expectedResponse: "String", + expectedError: nil, + }, + { + name: "Return substring at the end of the string with exact end index", + key: "SubStrKey2", + presetValue: "Test String Two", + command: []string{"SUBSTR", "SubStrKey2", "12", "14"}, + expectedResponse: "Two", + expectedError: nil, + }, + { + name: "Return substring at the end of the string with end index greater than length", + key: "SubStrKey3", + presetValue: "Test String Three", + command: []string{"SUBSTR", "SubStrKey3", "12", "75"}, + expectedResponse: "Three", + expectedError: nil, + }, + { + name: "Return the substring at the start of the string with 0 start index", + key: "SubStrKey4", + presetValue: "Test String Four", + command: []string{"SUBSTR", "SubStrKey4", "0", "3"}, + expectedResponse: "Test", + expectedError: nil, + }, + { + // Return the substring with negative start index. + // Substring should begin abs(start) from the end of the string when start is negative. + name: "Return the substring with negative start index", + key: "SubStrKey5", + presetValue: "Test String Five", + command: []string{"SUBSTR", "SubStrKey5", "-11", "10"}, + expectedResponse: "String", + expectedError: nil, + }, + { + // Return reverse substring with end index smaller than start index. + // When end index is smaller than start index, the 2 indices are reversed. + name: "Return reverse substring with end index smaller than start index", + key: "SubStrKey6", + presetValue: "Test String Six", + command: []string{"SUBSTR", "SubStrKey6", "4", "0"}, + expectedResponse: "tseT", + expectedError: nil, + }, + { + name: "Command too short", + command: []string{"SUBSTR", "key", "10"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "Command too long", + command: []string{"SUBSTR", "key", "10", "15", "20"}, + expectedError: errors.New(constants.WrongArgsResponse), + }, + { + name: "Start index is not an integer", + command: []string{"SUBSTR", "key", "start", "10"}, + expectedError: errors.New("start and end indices must be integers"), + }, + { + name: "End index is not an integer", + command: []string{"SUBSTR", "key", "0", "end"}, + expectedError: errors.New("start and end indices must be integers"), + }, + { + name: "Non-existent key", + command: []string{"SUBSTR", "non-existent-key", "0", "10"}, + expectedError: errors.New("key non-existent-key does not exist"), + }, + } - if res.Integer() != test.expectedResponse { - t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, res.Integer()) - } - }) - } -} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.presetValue != "" { + if err = client.WriteArray([]resp.Value{ + resp.StringValue("SET"), + resp.StringValue(test.key), + resp.StringValue(test.presetValue), + }); err != nil { + t.Error(err) + } + res, _, err := client.ReadValue() + if err != nil { + t.Error(err) + } -func Test_HandleSubStr(t *testing.T) { - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - t.Error(err) - } - client := resp.NewConn(conn) + if !strings.EqualFold(res.String(), "ok") { + t.Errorf("expected preset response to be OK, got %s", res.String()) + } + } - tests := []struct { - name string - key string - presetValue string - command []string - expectedResponse string - expectedError error - }{ - { - name: "Return substring within the range of the string", - key: "SubStrKey1", - presetValue: "Test String One", - command: []string{"SUBSTR", "SubStrKey1", "5", "10"}, - expectedResponse: "String", - expectedError: nil, - }, - { - name: "Return substring at the end of the string with exact end index", - key: "SubStrKey2", - presetValue: "Test String Two", - command: []string{"SUBSTR", "SubStrKey2", "12", "14"}, - expectedResponse: "Two", - expectedError: nil, - }, - { - name: "Return substring at the end of the string with end index greater than length", - key: "SubStrKey3", - presetValue: "Test String Three", - command: []string{"SUBSTR", "SubStrKey3", "12", "75"}, - expectedResponse: "Three", - expectedError: nil, - }, - { - name: "Return the substring at the start of the string with 0 start index", - key: "SubStrKey4", - presetValue: "Test String Four", - command: []string{"SUBSTR", "SubStrKey4", "0", "3"}, - expectedResponse: "Test", - expectedError: nil, - }, - { - // Return the substring with negative start index. - // Substring should begin abs(start) from the end of the string when start is negative. - name: "Return the substring with negative start index", - key: "SubStrKey5", - presetValue: "Test String Five", - command: []string{"SUBSTR", "SubStrKey5", "-11", "10"}, - expectedResponse: "String", - expectedError: nil, - }, - { - // Return reverse substring with end index smaller than start index. - // When end index is smaller than start index, the 2 indices are reversed. - name: "Return reverse substring with end index smaller than start index", - key: "SubStrKey6", - presetValue: "Test String Six", - command: []string{"SUBSTR", "SubStrKey6", "4", "0"}, - expectedResponse: "tseT", - expectedError: nil, - }, - { - name: "Command too short", - command: []string{"SUBSTR", "key", "10"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "Command too long", - command: []string{"SUBSTR", "key", "10", "15", "20"}, - expectedError: errors.New(constants.WrongArgsResponse), - }, - { - name: "Start index is not an integer", - command: []string{"SUBSTR", "key", "start", "10"}, - expectedError: errors.New("start and end indices must be integers"), - }, - { - name: "End index is not an integer", - command: []string{"SUBSTR", "key", "0", "end"}, - expectedError: errors.New("start and end indices must be integers"), - }, - { - name: "Non-existent key", - command: []string{"SUBSTR", "non-existent-key", "0", "10"}, - expectedError: errors.New("key non-existent-key does not exist"), - }, - } + command := make([]resp.Value, len(test.command)) + for i, c := range test.command { + command[i] = resp.StringValue(c) + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if test.presetValue != "" { - if err = client.WriteArray([]resp.Value{ - resp.StringValue("SET"), - resp.StringValue(test.key), - resp.StringValue(test.presetValue), - }); err != nil { + if err = client.WriteArray(command); err != nil { t.Error(err) } res, _, err := client.ReadValue() @@ -402,34 +437,17 @@ func Test_HandleSubStr(t *testing.T) { t.Error(err) } - if !strings.EqualFold(res.String(), "ok") { - t.Errorf("expected preset response to be OK, got %s", res.String()) + if test.expectedError != nil { + if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { + t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + } + return } - } - - command := make([]resp.Value, len(test.command)) - for i, c := range test.command { - command[i] = resp.StringValue(c) - } - - if err = client.WriteArray(command); err != nil { - t.Error(err) - } - res, _, err := client.ReadValue() - if err != nil { - t.Error(err) - } - if test.expectedError != nil { - if !strings.Contains(res.Error().Error(), test.expectedError.Error()) { - t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) + if res.String() != test.expectedResponse { + t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) } - return - } - - if res.String() != test.expectedResponse { - t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, res.String()) - } - }) - } + }) + } + }) } diff --git a/internal/raft/raft.go b/internal/raft/raft.go index 9baa1465..d06290d4 100644 --- a/internal/raft/raft.go +++ b/internal/raft/raft.go @@ -101,7 +101,7 @@ func (r *Raft) RaftInit(ctx context.Context) { addr, advertiseAddr, 10, - 10*time.Second, + 5*time.Second, os.Stdout, ) @@ -216,12 +216,13 @@ func (r *Raft) TakeSnapshot() error { } func (r *Raft) RaftShutdown() { - // Leadership transfer if current node is the leader + // Leadership transfer if current node is the leader. if r.IsRaftLeader() { err := r.raft.LeadershipTransfer().Error() if err != nil { - log.Fatal(err) + log.Printf("raft shutdown: %v\n", err) + return } - log.Println("Leadership transfer successful.") + log.Println("leadership transfer successful.") } } diff --git a/internal/utils.go b/internal/utils.go index 3b83e30c..9603f5db 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -18,6 +18,7 @@ import ( "bufio" "bytes" "cmp" + "crypto/tls" "errors" "fmt" "github.com/echovault/echovault/internal/constants" @@ -30,6 +31,7 @@ import ( "slices" "strconv" "strings" + "syscall" "time" "github.com/sethvargo/go-retry" @@ -427,3 +429,63 @@ func GetFreePort() (int, error) { return l.Addr().(*net.TCPAddr).Port, nil } + +func GetConnection(addr string, port int) (net.Conn, error) { + var conn net.Conn + var err error + done := make(chan struct{}) + + go func() { + for { + conn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil && errors.Is(err.(*net.OpError), syscall.ECONNREFUSED) { + // If we get a "connection refused error, try again." + continue + } + break + } + done <- struct{}{} + }() + + ticker := time.NewTicker(10 * time.Second) + defer func() { + ticker.Stop() + }() + + select { + case <-ticker.C: + return nil, errors.New("connection timeout") + case <-done: + return conn, err + } +} + +func GetTLSConnection(addr string, port int, config *tls.Config) (net.Conn, error) { + var conn net.Conn + var err error + done := make(chan struct{}) + + go func() { + for { + conn, err = tls.Dial("tcp", fmt.Sprintf("%s:%d", addr, port), config) + if err != nil && errors.Is(err.(*net.OpError), syscall.ECONNREFUSED) { + // If we get a "connection refused error, try again." + continue + } + break + } + done <- struct{}{} + }() + + ticker := time.NewTicker(10 * time.Second) + defer func() { + ticker.Stop() + }() + + select { + case <-ticker.C: + return nil, errors.New("connection timeout") + case <-done: + return conn, err + } +} diff --git a/openssl/client/cert.conf b/openssl/client/cert.conf new file mode 100644 index 00000000..9e348e31 --- /dev/null +++ b/openssl/client/cert.conf @@ -0,0 +1,8 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost + diff --git a/openssl/client/client1.crt b/openssl/client/client1.crt new file mode 100644 index 00000000..f7598a2f --- /dev/null +++ b/openssl/client/client1.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDsTCCApmgAwIBAgIUceiEXLKJyPYbsI8+rOECsoAjXMMwDQYJKoZIhvcNAQEL +BQAwODESMBAGA1UEAwwJbG9jYWxob3N0MQswCQYDVQQGEwJNWTEVMBMGA1UEBwwM +S3VhbGEgTHVtcHVyMB4XDTI0MDIwMjIxNDczMloXDTM0MDEzMDIxNDczMlowezEL +MAkGA1UEBhMCTVkxFTATBgNVBAgMDEt1YWxhIEx1bXB1cjEVMBMGA1UEBwwMS3Vh +bGEgTHVtcHVyMRIwEAYDVQQKDAlFY2hvVmF1bHQxFjAUBgNVBAsMDUVjaG9WYXVs +dCBEZXYxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANYPfUU/CyxkEK8jUCyRfXLhVWBvqTKqYojprhOEzmfizxs/osMA +8XQPHciuBCNIReOv7RfVd7os2mOpnjlubyiIEdJ18A6WefbjyDOiTFIZtpVxaEsb +du2/t7wPuyDdkfXC0l0agG8EbpcguKWnD0H+b9gwLX+CB046xNZlJm/rTAUycH4N +f7chr4awTp1ulag2AV7o+zgqZUpy4YpxDGkYJ42H24ehdZ7/l4JUDvJvt7aH49cP +BaT8LpUe8vIPeNlV1VnxS0499zZBYm9hbYAutOoKpsHXMwfVBqNB067oZi5xy14z +vROZ9U4FumkByZ+LPqv7gBHgmmC7HzPzqrcCAwEAAaNwMG4wHwYDVR0jBBgwFoAU +0ew0XSAHAYVSt4ncqBEWecMHZ2AwCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwFAYD +VR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdDgQWBBR4DSnn//dXMdhXhMtCWjY1GSDI +7jANBgkqhkiG9w0BAQsFAAOCAQEASOCH15wtjq9fJ7yVbjRRYnhS8mP7VS9s51m9 +G1PbgsV1P4xuElt4Md77xLthit45jW3rQuF+iMXpRYu9Fo5o1OQoIX4FE7fFofBg +Q/We+xVy8lrzDLbmg0MV0zg4urfYOrMqj6eYXSXzOOnzgvhH20yc+/KSXELv0YUP +a9kQgpf/4VA3yINH/+EcLkR2fP2ktzfL2l4PsfXFEzcKzOWIsXtMUo/wyJzLDjl1 +3fmUaMmNqBrKX+PM8uuXoT4/yBVHFA6vr96E5tZP6rid7xOgsonQ1Zf37ffhfXi/ +Umq6bGQK1eTfdq74pTWbNU59QfbWfR7LI+M32kQla//Ca3D/4g== +-----END CERTIFICATE----- diff --git a/openssl/client/client1.key b/openssl/client/client1.key new file mode 100644 index 00000000..f715cc19 --- /dev/null +++ b/openssl/client/client1.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDWD31FPwssZBCv +I1AskX1y4VVgb6kyqmKI6a4ThM5n4s8bP6LDAPF0Dx3IrgQjSEXjr+0X1Xe6LNpj +qZ45bm8oiBHSdfAOlnn248gzokxSGbaVcWhLG3btv7e8D7sg3ZH1wtJdGoBvBG6X +ILilpw9B/m/YMC1/ggdOOsTWZSZv60wFMnB+DX+3Ia+GsE6dbpWoNgFe6Ps4KmVK +cuGKcQxpGCeNh9uHoXWe/5eCVA7yb7e2h+PXDwWk/C6VHvLyD3jZVdVZ8UtOPfc2 +QWJvYW2ALrTqCqbB1zMH1QajQdOu6GYuccteM70TmfVOBbppAcmfiz6r+4AR4Jpg +ux8z86q3AgMBAAECggEAEeE6u1rgBSXmKnYLWQjdm7r/nyD+I7NaRjzCltj8E269 +K6j9xuA5/gUFncDcEdH6tUesfAlCfJeitdPLcz+d44Ea5mOGGPai542wjl0kyjzH +yyUtKwzduukaVAggvy0fgN6N8X61S9CcDiqKnprDXWWyLbKEfy1Uv+K/yBESpbyW +28s/QPmk5kd8a3YrcemHUQGp/QwjV1We0+TrCqdos4uaEZn6cCnrjQoXYRkMtGwZ +m9N+tHxAq5u7ZGoQnA9pHkAq+COyj03v4yP9tTSp4MJ3U/9auV2C+EyF8uYtM+qa +IU1pf3jp0D3BAEatGr/+/Yaz19GZx7w3FpA1NbHBPQKBgQDrbZoQQJS64IREGwRH +omqDubv+OT8ffuC2+7LEPsNbUabNLrV/FzAumRlI3Y7Eog8Hu1hg2HT6sJVzsR3S +u4f/OwR1a6I+6NU9ZLmx/s9qI0Lu7ah2x5QpKGBJo0VBGU/TWCl51cIAS9iXIcZg +OsdWDCzqHct4xKedYZsIjbpGwwKBgQDow+fKisofp/xMQPO3/7LJCBTsgK15TB6s +GX8InMqLKX1QpJpv1LvCrGNd+oYGit85fC8SFZuVXT2P65BRCQLfO5KioxoNuDGf +S0V+q/3+8BcsKUZVMChbau4nXhBeUI9twGwYHpbzFQEhgcrU8aXl/dS0fB/pwhJR +DxPKVRGU/QKBgQCMvv138eP4xPjN7ojkeojLL2LgXUELh0K4okkBYbRRB8N8rwv6 +atZ3RTgEg9AyZeAucyYm38EvjhoLDDwUG+D2CUZlHG/mxDOXfHw3mWpOvb3qMVKh +kDdXU7gczes9O/CpHO/O0qgknTNjRuHd7cX1/1lqrV1TWd4LDKsutexDGQKBgBsl +NbQGSZo1ghP2gzXTKSuOuLn4K8L4oJ8bfhgoCOr/1LCB8czW92q1pgUAwX6j1XKj +y+2E/ZcGv7Y4F6WLsn0MOoajFNfCwm68XYdvUXjY0SsCSUSIEDzRFKMcsjX9mSyI +g1KwxpPkwDQDKf95iwpuds7xptshGffAFWPEVf+VAoGBAObaWaeVibXnJbX/cAC0 +HT+NrAGMCITEoOD790SzFO+jsQAx9wDjbl3IkObHjSCgNmkqMk3GoVbgL7GeuiYE +dTp2G+308sQEIxASypUo/oyoZO+gbGToenK/JVTzTlh5+nrvdZFrfPCkO7Ljp5qr +NeZg9WiVOkpMQHPiPOw95asI +-----END PRIVATE KEY----- diff --git a/openssl/client/client2.crt b/openssl/client/client2.crt new file mode 100644 index 00000000..fb191b48 --- /dev/null +++ b/openssl/client/client2.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDsTCCApmgAwIBAgIUceiEXLKJyPYbsI8+rOECsoAjXMQwDQYJKoZIhvcNAQEL +BQAwODESMBAGA1UEAwwJbG9jYWxob3N0MQswCQYDVQQGEwJNWTEVMBMGA1UEBwwM +S3VhbGEgTHVtcHVyMB4XDTI0MDIwMjIxNDc1NFoXDTM0MDEzMDIxNDc1NFowezEL +MAkGA1UEBhMCTVkxFTATBgNVBAgMDEt1YWxhIEx1bXB1cjEVMBMGA1UEBwwMS3Vh +bGEgTHVtcHVyMRIwEAYDVQQKDAlFY2hvVmF1bHQxFjAUBgNVBAsMDUVjaG9WYXVs +dCBEZXYxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALojRky3CuIqbm03hSBCBefxCRE2XGk9ApZQILojt44BeoybJtxQ +01sbwXLHtr2SEd5ykr2/S9MOa5Rd3KXD2o2TuJ/RekBKHIn7KAVkf+5i+/PKmdlU +bMLPkdtSNTSfQCKXkoprJZe2kryrhu2pxJL18zu5ueaxuMEHY1//2wfeGJQG5DJ/ +6jR7SMl3ZVzv7wdX20sQgDijddYUzKderOHWxglIjPNxvLJIgPPa6zUYeE6LQZPH +dBwCYgd/BJzclXovof4+meUFWMs7T+ZQ+g6n9mv2QPOq95Ut+wxa8rfyyouQtzXj +7nKxTyiFtaEwIVIHWnefegZfgwbZCBle3KcCAwEAAaNwMG4wHwYDVR0jBBgwFoAU +0ew0XSAHAYVSt4ncqBEWecMHZ2AwCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwFAYD +VR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdDgQWBBRg/JsrbGLJKuPV1aCLvBVNgadS +GDANBgkqhkiG9w0BAQsFAAOCAQEAIs59eJnjq41PkevqcOaNJe0c/9GN+y8pWFfd +k9YZPu4HytTwylRaCOuPzGDpFtqBoVC4B1D7frw0oURd4zNjfxlwNTH2kfV8Uoz4 +GnWHHGvMrlzZLlGJnkuu8ciYjb2r2VEkLeSn8VDHpVIgLyxlHEOTkM866vZWD8WH +HWutpfc0cNPOMopQyWMe2S/jXmDwSA8t48iWlXAxLMEUA1OkF+jgz4CZ/c7Um64k +jsuSOSJgg6P12RFia2UDD9hzQWTgvxERuG9DjxJ8QKKLUKLRQ96EWpqamDoYb7vA +Kld5s3/EGd/zcrGzCt4mzdiMRVNOhskbWnifpRqNZouwvFlC4Q== +-----END CERTIFICATE----- diff --git a/openssl/client/client2.key b/openssl/client/client2.key new file mode 100644 index 00000000..478d1ec5 --- /dev/null +++ b/openssl/client/client2.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC6I0ZMtwriKm5t +N4UgQgXn8QkRNlxpPQKWUCC6I7eOAXqMmybcUNNbG8Fyx7a9khHecpK9v0vTDmuU +Xdylw9qNk7if0XpAShyJ+ygFZH/uYvvzypnZVGzCz5HbUjU0n0Ail5KKayWXtpK8 +q4btqcSS9fM7ubnmsbjBB2Nf/9sH3hiUBuQyf+o0e0jJd2Vc7+8HV9tLEIA4o3XW +FMynXqzh1sYJSIzzcbyySIDz2us1GHhOi0GTx3QcAmIHfwSc3JV6L6H+PpnlBVjL +O0/mUPoOp/Zr9kDzqveVLfsMWvK38sqLkLc14+5ysU8ohbWhMCFSB1p3n3oGX4MG +2QgZXtynAgMBAAECggEAArtSByVZU0WMEZAwYAMWz1S5XojMbxZ9yOHzUQHeB6TD +hwHkXnStrCr5OiuCPntKvBLSdkNGt8sKk64dbSP6gGhpeWzRoiPUTSmj8nOyobeA +Z1RX5K00eWqsNG9zsZkz8kG15wtMW/pd/8diWeVo+HoXzbYDCQHQAwzm0qYLk/tg ++k4hIlhEesf1SimyNmsEo9JOuzRcvM58+OvPmbUILcpT8HCOJPQ2XIZDJV6ln2o+ +AoXvF/01d7i/F0Iu+LUg1gVokvThcXZQhV00gkdZHZ1ENUj09hi2LPVCZ56PoWCT +WG93S50c6KGd1DhZ3AxjJM4TX9Y0XVcI39GuY8cuBQKBgQDthU0ZLrH8qKNPKL6j +lsC57I6+1bpVvbcvKvGflYP0LWUSIBHa86BLafKNH2/zumvZx6bQBCNQTo7T+FJV +IuD3oQlzA4L0trCbiQZxUftt2wNbL6DblnysDjWtiU1HbZFgcD5pW+vYxvHTG/I0 +BaCteGvS70ixRzZnYROQsr//gwKBgQDInpSD6iKn/hNvG4+TTEKaA4fTPTjxsNfh +j1w+CcCV8ibcAqJJI0wXcJs+QnCzlytinCjbo2ZzwKjOpN7spEQyTu8XtzNQNBO8 +7KfyuvyYOEqTvRfge2VRn9UvWtndOSXWJYGmnewHkh4jLt+19NgoHaBFzYLCr0oD +uKog/WUhDQKBgQDf8NSWL56ElwMSeVn0pwgiw9R6PMyoVmzGPfj9+1wj9kDa6/2p +sBWrxMJ5J/DHnTZeaIzwh1Y8OzUSyYfm2TG+h8h+9gqcazrsCi9W3HLwSpRJfwhs +wN/e4K7fZRrFg5qTkIBnmdEt27TY0/px7fRmWalfgVfKPVgf9DkcLkwzvQKBgQCq +chC8ArBvCe5493GEM8ZiE53SWrGGpjjD6oj0LFTzEEjzo0k92j9LquA6hTg7XLP/ +k60i7jCdJ5JD/s9nPiiylV2NSJjQC265lFccYsE4kprJ6l3e2ve54ZG+KfHvgh4j +UrpUVNezlvED808dyGfdrU3+AByYS1UW1E22uZKyAQKBgQCpy7evEHTwA+GeAAuq +3pLtyzQHO9bCk+TbccJbdlKQ6py9Cm4WSO3NuQLy5yW/+WnGpR4HQegGoUmxKcww +u3GGmxlPWqbz9NVGcs09FBwzyB0VTNOnp5RkFq3jS7YOFC5j55g4d3OpAsoeipP0 +OSEceMqSURM/FRFkgSh1Dy4C4A== +-----END PRIVATE KEY----- diff --git a/openssl/client/csr.conf b/openssl/client/csr.conf new file mode 100644 index 00000000..2988f278 --- /dev/null +++ b/openssl/client/csr.conf @@ -0,0 +1,24 @@ +[ req ] +default_bits = 2048 +prompt = no +default_md = sha256 +req_extensions = req_ext +distinguished_name = dn + +[ dn ] +C = MY +ST = Kuala Lumpur +L = Kuala Lumpur +O = EchoVault +OU = EchoVault Dev +CN = localhost + +[ req_ext ] +subjectAltName = @alt_names + +[ alt_names ] +DNS.1 = localhost +DNS.2 = localhost +IP.1 = 192.168.1.5 +IP.2 = 192.168.1.6 + diff --git a/openssl/client/rootCA.key b/openssl/client/rootCA.key new file mode 100644 index 00000000..e2ec82c0 --- /dev/null +++ b/openssl/client/rootCA.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDZgeqh2ogpWcHW +MhjhiyMhYcRltPdicN005C7beIy/x8HZ2HAEkYCWe9ffFn5RORsqwF8wrol8d8jV +fstw9rGz3fHyIqz2dSnfB3IJO7Nvgekf8p7W1brHeQaVb6EsQ0LVhGAYzdJNi5NB +y1bYQqBAk8H6caSSniOhrUNz5KF0pYJ9evJehWmLyWNDFqKpjyRDxOn8WwOMra0e +0bI8bTRd8iun3A9qqWAdrxRMR2niBKM/G7akX1OSRJ2mfLkj6Pt9OujJDY+T6Y8r +wRoGPSOrELsaI6UcRO20Oafzig7MPEpldX1n07X6v9OYREHj36nszoBveigQNSTR +WZmL9rGFAgMBAAECggEAPuCVNRPpD9cgN20FD1J7Jd/O+D3v0/fforYiK5T2T0yO +aAzvGQr8+sOzXIzymEVjaqDxA7A5E4/HMZy1cCMIrQAIvOA0Uwz8vTo4R54IGcCa +5X7sVxuzIo4EjreWBqctD290nkcFuCAUwkznfp4IGJL+XQl0M2Re1ZKycLLTz9Wu +Q+e5JvUzA1fn7Va2nk/vf2uuEhkLA+He1nY1pXt7AS6OH3XHkYV2ZGugJGkxM9I7 +dWFzpPcyNRN5yA4WUql9nOj7giHT313HQ1j3UPeoZ6NY7TRPQwT+iZQszHcie2Md +WSw2cH+W7TuF4MAhwc5rsvDjGmmdq5cstWqMHGBBcwKBgQD3oDKU0p5Tq7HjJV5I +XBAALdLg0+dsZPDVvpi2JY6j3TWESWEudrl2g3Kx2wJfowa8O76/9jzceX2k1nek +1r8BVgDCkfFrWN3bVDzf28h/+1ywXPepcKwrNufShdoJQYQ8nfafE4gDTO1Snej6 +81ZuwKS5Rt7Q9JHHrh3G/15RjwKBgQDg3PjYCoXlyLPuK/Co4JJ1phZIdAucVN0v +56g862C8nK/FtgdHhZLoU797PnqIxQeE/E+URSwQP/lvokMAcQdM7oqJMT9sJePT +VnFLR76DfuZSJQ8dPM4C8WHF9ioGtdzmKeYtJP639T/uz2Z+CZQ3eNMrWykgDjO9 +gBnrW+zZqwKBgG9ccwLsyVk1kNVnO8Rs6qE5+mkzwxLDPm/RvFnGACT/WY75dSPx +Lqz2poEHzkR2S5QhhkJMGcjJNlEIRlwyW0ndhI/8FEdDetqlQo8mB0BPKbsCxDpG +OpdgpNbPbWPWPAMKwxt9LCDX2q7Z5yncf1Vle277ST9NjbXwPuH8fE1PAoGAEVnb +tcfyFw4KnEk1s8JIat2bAJI7xx9hRe4JNFIxT7yDb60hGKq88EJuFxN2HxGdB+z0 +Mwu3X7WgCLYrl2AhYRVTCU0MiMrPrqIP8fAiSkFDgnkrlmT3vJBlrAHXslbcKcJ3 +6WneYdGB0mqcjQMuNa2UFddd8ARIh8nXtiqMtysCgYB1BD89V8ivMACkuy5FGe8k +2kFSUI8DSVrXRPZ2mRCTho/lbIYvpIXY2qfnz+PZpyf80JdRu4zfaAhxXP2r08+z +3+bFHnI3OYBI2M6pLWf48HYJfW7UawCW4BMlisb3EiXAz1vsUWgR9I0wLa7MRfkP +YW0ZPyWOl8+eIR2BZo7dQA== +-----END PRIVATE KEY----- diff --git a/openssl/client/rootCA.srl b/openssl/client/rootCA.srl new file mode 100644 index 00000000..4fcdcffe --- /dev/null +++ b/openssl/client/rootCA.srl @@ -0,0 +1 @@ +71E8845CB289C8F61BB08F3EACE102B280235CC4