Files
athens/pkg/stash/with_redis_test.go
opalmer 3ba08f64f0 Support using redis urls to construct the redis client (#1994)
Currently Athens only supports connecting to Redis using a hostname:port combination in addition to a password. While this works in most cases it also means that if you have other options you wish to supply Athens has to be updated to support them. As a basic example Redis clusters that require TLS currently are not supported by Athens but with this change you can simply supply a [redis url](https://github.com/redis/redis-specifications/blob/master/uri/redis.txt) to connect over TLS. It also makes it easy to override the password, set a username and more all from a single configuration option:

`rediss://username:password@redis.example.com:6379/1?protocol=3`
2024-10-22 13:33:03 -07:00

223 lines
5.5 KiB
Go

package stash
import (
"context"
"fmt"
"os"
"strings"
"sync"
"testing"
"time"
"github.com/go-redis/redis/v8"
"github.com/gomods/athens/pkg/config"
"github.com/gomods/athens/pkg/errors"
"github.com/gomods/athens/pkg/storage"
"github.com/gomods/athens/pkg/storage/mem"
"golang.org/x/sync/errgroup"
)
// testingRedisLogger implements pkg/stash.RedisLogger.
type testingRedisLogger struct {
t *testing.T
}
func (l *testingRedisLogger) Printf(ctx context.Context, format string, v ...any) {
l.t.Logf(format, v...)
}
// WithRedisLock will ensure that 5 concurrent requests will all get the first request's
// response. We can ensure that because only the first response does not return an error
// and therefore all 5 responses should have no error.
func TestWithRedisLock(t *testing.T) {
endpoint := os.Getenv("REDIS_TEST_ENDPOINT")
password := os.Getenv("ATHENS_REDIS_PASSWORD")
if len(endpoint) == 0 {
t.SkipNow()
}
strg, err := mem.NewStorage()
if err != nil {
t.Fatal(err)
}
ms := &mockRedisStasher{strg: strg}
l := &testingRedisLogger{t: t}
wrapper, err := WithRedisLock(l, endpoint, password, storage.WithChecker(strg), config.DefaultRedisLockConfig())
if err != nil {
t.Fatal(err)
}
s := wrapper(ms)
var eg errgroup.Group
for i := 0; i < 5; i++ {
eg.Go(func() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := s.Stash(ctx, "mod", "ver")
return err
})
}
err = eg.Wait()
if err != nil {
t.Fatal(err)
}
}
// Verify with WithRedisLock working with password protected redis
// Same logic as the TestWithRedisLock test.
func TestWithRedisLockWithPassword(t *testing.T) {
endpoint := os.Getenv("PROTECTED_REDIS_TEST_ENDPOINT")
password := os.Getenv("ATHENS_PROTECTED_REDIS_PASSWORD")
if len(endpoint) == 0 {
t.SkipNow()
}
strg, err := mem.NewStorage()
if err != nil {
t.Fatal(err)
}
ms := &mockRedisStasher{strg: strg}
l := &testingRedisLogger{t: t}
wrapper, err := WithRedisLock(l, endpoint, password, storage.WithChecker(strg), config.DefaultRedisLockConfig())
if err != nil {
t.Fatal(err)
}
s := wrapper(ms)
var eg errgroup.Group
for i := 0; i < 5; i++ {
eg.Go(func() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := s.Stash(ctx, "mod", "ver")
return err
})
}
err = eg.Wait()
if err != nil {
t.Fatal(err)
}
}
// Verify the WithRedisLock fails with the correct error when trying
// to connect with the wrong password.
func TestWithRedisLockWithWrongPassword(t *testing.T) {
endpoint := os.Getenv("PROTECTED_REDIS_TEST_ENDPOINT")
password := ""
if len(endpoint) == 0 {
t.SkipNow()
}
strg, err := mem.NewStorage()
if err != nil {
t.Fatal(err)
}
l := &testingRedisLogger{t: t}
_, err = WithRedisLock(l, endpoint, password, storage.WithChecker(strg), config.DefaultRedisLockConfig())
if err == nil {
t.Fatal("Expected Connection Error")
}
if !strings.Contains(err.Error(), "NOAUTH Authentication required.") {
t.Fatalf("Wrong error was thrown %q\n", err.Error())
}
}
type getRedisClientOptionsFacet struct {
endpoint string
password string
options *redis.Options
err error
}
func Test_getRedisClientOptions(t *testing.T) {
facets := []*getRedisClientOptionsFacet{
{
endpoint: "127.0.0.1:6379",
options: &redis.Options{
Addr: "127.0.0.1:6379",
},
},
{
endpoint: "127.0.0.1:6379",
password: "1234",
options: &redis.Options{
Addr: "127.0.0.1:6379",
Password: "1234",
},
},
{
endpoint: "rediss://username:password@127.0.0.1:6379",
password: "1234", // Ignored because password was parsed
err: errors.E("stash.WithRedisLock", errPasswordsDoNotMatch),
},
{
endpoint: "rediss://username:password@127.0.0.1:6379",
password: "1234", // Ignored because password was parsed
err: errors.E("stash.WithRedisLock", errPasswordsDoNotMatch),
},
}
for i, facet := range facets {
options, err := getRedisClientOptions(facet.endpoint, facet.password)
if err != nil && facet.err == nil {
t.Errorf("Facet %d: no error produced", i)
continue
}
if facet.err != nil {
if err == nil {
t.Errorf("Facet %d: no error produced", i)
} else {
if err.Error() != facet.err.Error() {
t.Errorf("Facet %d: expected %q, got %q", i, facet.err, err)
}
}
}
if err != nil {
continue
}
if facet.options.Addr != options.Addr {
t.Errorf("Facet %d: Expected Addr %q, got %q", i, facet.options.Addr, options.Addr)
}
if facet.options.Username != options.Username {
t.Errorf("Facet %d: Expected Username %q, got %q", i, facet.options.Username, options.Username)
}
if facet.options.Password != options.Password {
t.Errorf("Facet %d: Expected Password %q, got %q", i, facet.options.Password, options.Password)
}
}
}
// mockRedisStasher is like mockStasher
// but leverages in memory storage
// so that redis can determine
// whether to call the underlying stasher or not.
type mockRedisStasher struct {
strg storage.Backend
mu sync.Mutex
num int
}
func (ms *mockRedisStasher) Stash(ctx context.Context, mod, ver string) (string, error) {
time.Sleep(time.Millisecond * 100) // allow for second requests to come in.
ms.mu.Lock()
defer ms.mu.Unlock()
if ms.num == 0 {
err := ms.strg.Save(
ctx,
mod,
ver,
[]byte("mod file"),
strings.NewReader("zip file"),
[]byte("info file"),
)
if err != nil {
return "", err
}
ms.num++
return "", nil
}
return "", fmt.Errorf("second time error")
}