package stop import ( "context" "errors" "fmt" "io" "net/http" "testing" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/volume" "github.com/docker/docker/client" "github.com/h2non/gock" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/testing/apitest" "github.com/supabase/cli/internal/utils" ) func TestStopCommand(t *testing.T) { t.Run("stops containers with backup", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/json"). Reply(http.StatusOK). JSON([]types.Container{}) gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/prune"). Reply(http.StatusOK). JSON(container.PruneReport{}) gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/networks/prune"). Reply(http.StatusOK). JSON(network.PruneReport{}) gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/volumes"). Reply(http.StatusOK). JSON(volume.ListResponse{Volumes: []*volume.Volume{{ Name: utils.DbId, }}}) // Run test err := Run(context.Background(), true, "", false, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("stops all instances when --all flag is used", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() projects := []string{"project1", "project2"} // Mock initial ContainerList for all containers gock.New(utils.Docker.DaemonHost()). Get("/v"+utils.Docker.ClientVersion()+"/containers/json"). MatchParam("all", "true"). Reply(http.StatusOK). JSON([]types.Container{ {ID: "container1", Labels: map[string]string{utils.CliProjectLabel: "project1"}}, {ID: "container2", Labels: map[string]string{utils.CliProjectLabel: "project2"}}, }) // Mock initial VolumeList gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/volumes"). Reply(http.StatusOK). JSON(volume.ListResponse{ Volumes: []*volume.Volume{ {Name: "volume1", Labels: map[string]string{utils.CliProjectLabel: "project1"}}, {Name: "volume2", Labels: map[string]string{utils.CliProjectLabel: "project2"}}, }, }) // Mock stopOneProject for each project for _, projectId := range projects { // Mock ContainerList for each project gock.New(utils.Docker.DaemonHost()). Get("/v"+utils.Docker.ClientVersion()+"/containers/json"). MatchParam("all", "1"). MatchParam("filters", fmt.Sprintf(`{"label":{"com.supabase.cli.project=%s":true}}`, projectId)). Reply(http.StatusOK). JSON([]types.Container{{ID: "container-" + projectId, State: "running"}}) // Mock container stop gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/container-" + projectId + "/stop"). Reply(http.StatusOK) gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/prune"). Reply(http.StatusOK). JSON(container.PruneReport{}) gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/networks/prune"). Reply(http.StatusOK). JSON(network.PruneReport{}) gock.New(utils.Docker.DaemonHost()). Get("/v"+utils.Docker.ClientVersion()+"/volumes"). MatchParam("filters", fmt.Sprintf(`{"label":{"com.supabase.cli.project=%s":true}}`, projectId)). Reply(http.StatusOK). JSON(volume.ListResponse{Volumes: []*volume.Volume{{Name: "volume-" + projectId}}}) } // Mock final ContainerList to verify all containers are stopped gock.New(utils.Docker.DaemonHost()). Get("/v"+utils.Docker.ClientVersion()+"/containers/json"). MatchParam("all", "true"). Reply(http.StatusOK). JSON([]types.Container{}) gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/json"). Reply(http.StatusOK). JSON([]types.Container{}) // Run test err := Run(context.Background(), true, "", true, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on malformed config", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, afero.WriteFile(fsys, utils.ConfigPath, []byte("malformed"), 0644)) // Run test err := Run(context.Background(), false, "", false, fsys) // Check error assert.ErrorContains(t, err, "toml: expected = after a key, but the document ends there") }) t.Run("throws error on stop failure", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/json"). Reply(http.StatusServiceUnavailable) // Run test err := Run(context.Background(), false, "test", false, afero.NewReadOnlyFs(fsys)) // Check error assert.ErrorContains(t, err, "request returned Service Unavailable for API route and version") assert.Empty(t, apitest.ListUnmatchedRequests()) }) } func TestStopServices(t *testing.T) { t.Run("stops all services", func(t *testing.T) { containers := []types.Container{{ID: "c1", State: "running"}, {ID: "c2"}} // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/json"). Reply(http.StatusOK). JSON(containers) gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/" + containers[0].ID + "/stop"). Reply(http.StatusOK) gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/prune"). Reply(http.StatusOK). JSON(container.PruneReport{}) gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/networks/prune"). Reply(http.StatusOK). JSON(network.PruneReport{}) // Run test err := stop(context.Background(), true, io.Discard, utils.Config.ProjectId) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("removes data volumes", func(t *testing.T) { utils.DbId = "test-db" utils.ConfigId = "test-config" utils.StorageId = "test-storage" utils.EdgeRuntimeId = "test-functions" utils.InbucketId = "test-inbucket" // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() apitest.MockDockerStop(utils.Docker) // Run test err := stop(context.Background(), false, io.Discard, utils.Config.ProjectId) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("skips all filter when removing data volumes with Docker version pre-v1.42", func(t *testing.T) { utils.DbId = "test-db" utils.ConfigId = "test-config" utils.StorageId = "test-storage" utils.EdgeRuntimeId = "test-functions" utils.InbucketId = "test-inbucket" // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) require.NoError(t, client.WithVersion("1.41")(utils.Docker)) defer gock.OffAll() apitest.MockDockerStop(utils.Docker) // Run test err := stop(context.Background(), false, io.Discard, utils.Config.ProjectId) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) }) t.Run("throws error on prune failure", func(t *testing.T) { // Setup mock docker require.NoError(t, apitest.MockDocker(utils.Docker)) defer gock.OffAll() gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/json"). Reply(http.StatusOK). JSON([]types.Container{}) gock.New(utils.Docker.DaemonHost()). Post("/v" + utils.Docker.ClientVersion() + "/containers/prune"). ReplyError(errors.New("network error")) // Run test err := stop(context.Background(), true, io.Discard, utils.Config.ProjectId) // Check error assert.ErrorContains(t, err, "network error") assert.Empty(t, apitest.ListUnmatchedRequests()) }) }