Skip to content

Toxiproxy

package toxiproxy

import (
    "context"
    "fmt"

    "github.com/testcontainers/testcontainers-go/wait"

    "github.com/testcontainers/testcontainers-go"
)

// toxiproxyContainer represents the toxiproxy container type used in the module
type toxiproxyContainer struct {
    testcontainers.Container
    URI string
}

// startContainer creates an instance of the toxiproxy container type
func startContainer(ctx context.Context, network string, networkAlias []string) (*toxiproxyContainer, error) {
    req := testcontainers.ContainerRequest{
        Image:        "ghcr.io/shopify/toxiproxy:2.5.0",
        ExposedPorts: []string{"8474/tcp", "8666/tcp"},
        WaitingFor:   wait.ForHTTP("/version").WithPort("8474/tcp"),
        Networks: []string{
            network,
        },
        NetworkAliases: map[string][]string{
            network: networkAlias,
        },
    }
    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    if err != nil {
        return nil, err
    }

    mappedPort, err := container.MappedPort(ctx, "8474")
    if err != nil {
        return nil, err
    }

    hostIP, err := container.Host(ctx)
    if err != nil {
        return nil, err
    }

    uri := fmt.Sprintf("%s:%s", hostIP, mappedPort.Port())

    return &toxiproxyContainer{Container: container, URI: uri}, nil
}
package toxiproxy

import (
    "context"
    "fmt"
    toxiproxy "github.com/Shopify/toxiproxy/v2/client"
    "github.com/go-redis/redis/v8"
    "github.com/google/uuid"
    "github.com/testcontainers/testcontainers-go"
    "testing"
    "time"
)

func TestToxiproxy(t *testing.T) {
    ctx := context.Background()

    newNetwork, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{
        ProviderType: testcontainers.ProviderDocker,
        NetworkRequest: testcontainers.NetworkRequest{
            Name:           "newNetwork",
            CheckDuplicate: true,
        },
    })
    if err != nil {
        t.Fatal(err)
    }

    toxiproxyContainer, err := startContainer(ctx, "newNetwork", []string{"toxiproxy"})
    if err != nil {
        t.Fatal(err)
    }

    redisContainer, err := setupRedis(ctx, "newNetwork", []string{"redis"})
    if err != nil {
        t.Fatal(err)
    }

    // Clean up the container after the test is complete
    t.Cleanup(func() {
        if err := toxiproxyContainer.Terminate(ctx); err != nil {
            t.Fatalf("failed to terminate container: %s", err)
        }
        if err := redisContainer.Terminate(ctx); err != nil {
            t.Fatalf("failed to terminate container: %s", err)
        }
        if err := newNetwork.Remove(ctx); err != nil {
            t.Fatalf("failed to terminate network: %s", err)
        }
    })

    toxiproxyClient := toxiproxy.NewClient(toxiproxyContainer.URI)
    proxy, err := toxiproxyClient.CreateProxy("redis", "0.0.0.0:8666", "redis:6379")
    if err != nil {
        t.Fatal(err)
    }

    toxiproxyProxyPort, err := toxiproxyContainer.MappedPort(ctx, "8666")
    if err != nil {
        t.Fatal(err)
    }

    toxiproxyProxyHostIP, err := toxiproxyContainer.Host(ctx)
    if err != nil {
        t.Fatal(err)
    }

    redisUri := fmt.Sprintf("redis://%s:%s?read_timeout=2s", toxiproxyProxyHostIP, toxiproxyProxyPort.Port())

    options, err := redis.ParseURL(redisUri)
    if err != nil {
        t.Fatal(err)
    }
    redisClient := redis.NewClient(options)
    defer flushRedis(ctx, *redisClient)

    // Set data
    key := fmt.Sprintf("{user.%s}.favoritefood", uuid.NewString())
    value := "Cabbage Biscuits"
    ttl, _ := time.ParseDuration("2h")
    err = redisClient.Set(ctx, key, value, ttl).Err()
    if err != nil {
        t.Fatal(err)
    }

    _, err = proxy.AddToxic("latency_down", "latency", "downstream", 1.0, toxiproxy.Attributes{
        "latency": 1000,
        "jitter":  100,
    })
    if err != nil {
        return
    }

    // Get data
    savedValue, err := redisClient.Get(ctx, key).Result()
    if err != nil {
        t.Fatal(err)
    }

    // perform assertions
    if savedValue != value {
        t.Fatalf("Expected value %s. Got %s.", savedValue, value)
    }
}

func flushRedis(ctx context.Context, client redis.Client) error {
    return client.FlushAll(ctx).Err()
}