package utils import ( "bytes" "context" "net/http" "testing" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/stdcopy" "github.com/h2non/gock" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/testing/apitest" ) const ( containerId = "test-container" imageId = "test-image" ) func TestPullImage(t *testing.T) { viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io") t.Run("pulls image if missing", func(t *testing.T) { // Setup mock docker require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() gock.New(Docker.DaemonHost()). Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). Reply(http.StatusNotFound) gock.New(Docker.DaemonHost()). Post("/v"+Docker.ClientVersion()+"/images/create"). MatchParam("fromImage", imageId). MatchParam("tag", "latest"). Reply(http.StatusAccepted) // Run test assert.NoError(t, DockerPullImageIfNotCached(context.Background(), imageId)) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("does nothing if image exists", func(t *testing.T) { // Setup mock docker require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() gock.New(Docker.DaemonHost()). Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). Reply(http.StatusOK). JSON(types.ImageInspect{}) // Run test assert.NoError(t, DockerPullImageIfNotCached(context.Background(), imageId)) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error if docker is unavailable", func(t *testing.T) { // Setup mock docker require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() gock.New(Docker.DaemonHost()). Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). Reply(http.StatusServiceUnavailable) // Run test assert.Error(t, DockerPullImageIfNotCached(context.Background(), imageId)) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on failure to pull image", func(t *testing.T) { timeUnit = time.Duration(0) // Setup mock docker require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() gock.New(Docker.DaemonHost()). Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). Reply(http.StatusNotFound) // Total 3 tries gock.New(Docker.DaemonHost()). Post("/v"+Docker.ClientVersion()+"/images/create"). MatchParam("fromImage", imageId). MatchParam("tag", "latest"). Reply(http.StatusServiceUnavailable) gock.New(Docker.DaemonHost()). Post("/v"+Docker.ClientVersion()+"/images/create"). MatchParam("fromImage", imageId). MatchParam("tag", "latest"). Reply(http.StatusAccepted). JSON(jsonmessage.JSONMessage{Error: &jsonmessage.JSONError{Message: "toomanyrequests"}}) gock.New(Docker.DaemonHost()). Post("/v"+Docker.ClientVersion()+"/images/create"). MatchParam("fromImage", imageId). MatchParam("tag", "latest"). Reply(http.StatusAccepted). JSON(jsonmessage.JSONMessage{Error: &jsonmessage.JSONError{Message: "no space left on device"}}) // Run test err := DockerPullImageIfNotCached(context.Background(), imageId) // Validate api assert.ErrorContains(t, err, "no space left on device") assert.Empty(t, apitest.ListUnmatchedRequests()) }) } func TestRunOnce(t *testing.T) { viper.Set("INTERNAL_IMAGE_REGISTRY", "docker.io") t.Run("runs once in container", func(t *testing.T) { // Setup mock docker require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() apitest.MockDockerStart(Docker, imageId, containerId) require.NoError(t, apitest.MockDockerLogs(Docker, containerId, "hello world")) // Run test out, err := DockerRunOnce(context.Background(), imageId, nil, nil) assert.NoError(t, err) // Validate api assert.Equal(t, "hello world", out) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on container create", func(t *testing.T) { // Setup mock docker require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() gock.New(Docker.DaemonHost()). Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). Reply(http.StatusOK). JSON(types.ImageInspect{}) gock.New(Docker.DaemonHost()). Post("/v" + Docker.ClientVersion() + "/networks/create"). Reply(http.StatusCreated). JSON(network.CreateResponse{}) gock.New(Docker.DaemonHost()). Post("/v" + Docker.ClientVersion() + "/containers/create"). Reply(http.StatusServiceUnavailable) // Run test _, err := DockerRunOnce(context.Background(), imageId, nil, nil) assert.Error(t, err) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on container start", func(t *testing.T) { // Setup mock docker require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() gock.New(Docker.DaemonHost()). Get("/v" + Docker.ClientVersion() + "/images/" + imageId + "/json"). Reply(http.StatusOK). JSON(types.ImageInspect{}) gock.New(Docker.DaemonHost()). Post("/v" + Docker.ClientVersion() + "/networks/create"). Reply(http.StatusCreated). JSON(network.CreateResponse{}) gock.New(Docker.DaemonHost()). Post("/v" + Docker.ClientVersion() + "/containers/create"). Reply(http.StatusOK). JSON(container.CreateResponse{ID: containerId}) gock.New(Docker.DaemonHost()). Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/start"). Reply(http.StatusServiceUnavailable) // Run test _, err := DockerRunOnce(context.Background(), imageId, nil, nil) assert.Error(t, err) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("removes container on cancel", func(t *testing.T) { // Setup mock docker require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() apitest.MockDockerStart(Docker, imageId, containerId) gock.New(Docker.DaemonHost()). Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs"). Reply(http.StatusOK). SetHeader("Content-Type", "application/vnd.docker.raw-stream"). Delay(1 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(200*time.Millisecond)) defer cancel() gock.New(Docker.DaemonHost()). Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId). Reply(http.StatusOK) // Run test _, err := DockerRunOnce(ctx, imageId, nil, nil) assert.Error(t, err) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on failure to parse logs", func(t *testing.T) { // Setup mock docker require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() apitest.MockDockerStart(Docker, imageId, containerId) gock.New(Docker.DaemonHost()). Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs"). Reply(http.StatusOK). SetHeader("Content-Type", "application/vnd.docker.raw-stream"). BodyString("hello world") gock.New(Docker.DaemonHost()). Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId). Reply(http.StatusOK) // Run test _, err := DockerRunOnce(context.Background(), imageId, nil, nil) assert.Error(t, err) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on failure to inspect", func(t *testing.T) { // Setup mock docker require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() apitest.MockDockerStart(Docker, imageId, containerId) // Setup docker style logs var body bytes.Buffer writer := stdcopy.NewStdWriter(&body, stdcopy.Stdout) _, err := writer.Write([]byte("hello world")) require.NoError(t, err) gock.New("http:///var/run/docker.sock"). Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs"). Reply(http.StatusOK). SetHeader("Content-Type", "application/vnd.docker.raw-stream"). Body(&body) gock.New("http:///var/run/docker.sock"). Get("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/json"). Reply(http.StatusServiceUnavailable) gock.New(Docker.DaemonHost()). Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId). Reply(http.StatusOK) // Run test _, err = DockerRunOnce(context.Background(), imageId, nil, nil) assert.ErrorContains(t, err, "request returned Service Unavailable for API route and version") // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on non-zero exit code", func(t *testing.T) { // Setup mock docker require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() apitest.MockDockerStart(Docker, imageId, containerId) // Setup docker style logs var body bytes.Buffer writer := stdcopy.NewStdWriter(&body, stdcopy.Stdout) _, err := writer.Write([]byte("hello world")) require.NoError(t, err) gock.New("http:///var/run/docker.sock"). Get("/v"+Docker.ClientVersion()+"/containers/"+containerId+"/logs"). Reply(http.StatusOK). SetHeader("Content-Type", "application/vnd.docker.raw-stream"). Body(&body) gock.New("http:///var/run/docker.sock"). Get("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/json"). Reply(http.StatusOK). JSON(types.ContainerJSONBase{State: &types.ContainerState{ExitCode: 1}}) gock.New(Docker.DaemonHost()). Delete("/v" + Docker.ClientVersion() + "/containers/" + containerId). Reply(http.StatusOK) // Run test _, err = DockerRunOnce(context.Background(), imageId, nil, nil) assert.ErrorContains(t, err, "error running container: exit 1") // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) } func TestExecOnce(t *testing.T) { t.Run("throws error on failure to exec", func(t *testing.T) { // Setup mock server require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() gock.New(Docker.DaemonHost()). Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/exec"). Reply(http.StatusServiceUnavailable) // Run test _, err := DockerExecOnce(context.Background(), containerId, nil, nil) assert.Error(t, err) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on failure to hijack", func(t *testing.T) { // Setup mock server require.NoError(t, apitest.MockDocker(Docker)) defer gock.OffAll() gock.New(Docker.DaemonHost()). Post("/v" + Docker.ClientVersion() + "/containers/" + containerId + "/exec"). Reply(http.StatusAccepted). JSON(types.IDResponse{ID: "test-command"}) // Run test _, err := DockerExecOnce(context.Background(), containerId, nil, nil) assert.Error(t, err) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) // TODO: mock tcp hijack }