Repository: supabase/cli
Pull request #516 (2022-10-12 16:00:17)

chore: add tests for stop command
## What kind of change does this PR introduce?

tests

## What is the new behavior?

- add unit tests

## Additional context

Add any other context or screenshots.


coveralls: ## Pull Request Test Coverage Report for [Build 3236138567](https://coveralls.io/builds/53215049)

* **31** of **35**   **(88.57%)**  changed or added relevant lines in **3** files are covered.
* **61** unchanged lines in **1** file lost coverage.
* Overall coverage increased (+**1.04%**) to **47.869%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [cmd/stop.go](https://coveralls.io/builds/53215049/source?filename=cmd%2Fstop.go#L16) | 0 | 4 | 0.0%
<!-- | **Total:** | **31** | **35** | **88.57%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/utils/misc.go](https://coveralls.io/builds/53215049/source?filename=internal%2Futils%2Fmisc.go#L113) | 61 | 65.95% |
<!-- | **Total:** | **61** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/53215049/badge)](https://coveralls.io/builds/53215049) |
| :-- | --: |
| Change from base [Build 3236082765](https://coveralls.io/builds/53214818): |  1.04% |
| Covered Lines: | 2347 |
| Relevant Lines: | 4903 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/cmd/stop.go b/cmd/stop.go
index 0de218ae..fafba5ad 100644
--- a/cmd/stop.go
+++ b/cmd/stop.go
@@ -1,17 +1,24 @@
 package cmd
 
 import (
+	"os"
+	"os/signal"
+
+	"github.com/spf13/afero"
 	"github.com/spf13/cobra"
 	"github.com/supabase/cli/internal/stop"
 )
 
-var stopCmd = &cobra.Command{
-	Use:   "stop",
-	Short: "Stop all local Supabase containers",
-	RunE: func(cmd *cobra.Command, args []string) error {
-		return stop.Run()
-	},
-}
+var (
+	stopCmd = &cobra.Command{
+		Use:   "stop",
+		Short: "Stop all local Supabase containers",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
+			return stop.Run(ctx, afero.NewOsFs())
+		},
+	}
+)
 
 func init() {
 	rootCmd.AddCommand(stopCmd)
diff --git a/internal/stop/stop.go b/internal/stop/stop.go
index 91b49fe0..f106f2b1 100644
--- a/internal/stop/stop.go
+++ b/internal/stop/stop.go
@@ -3,71 +3,59 @@ package stop
 import (
 	"context"
 	"fmt"
-	"os"
-	"sync"
+	"path/filepath"
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
+	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/utils"
 )
 
-var ctx = context.Background()
-
-func Run() error {
+func Run(ctx context.Context, fsys afero.Fs) error {
 	// Sanity checks.
-	if err := utils.AssertDockerIsRunning(); err != nil {
-		return err
-	}
-	if err := utils.LoadConfig(); err != nil {
-		return err
-	}
-	if err := utils.AssertSupabaseStartIsRunning(); err != nil {
-		fmt.Println(utils.Aqua("supabase") + " local development setup is already stopped.")
-		return nil
-	}
-
-	// Remove containers.
 	{
-		containers, err := utils.Docker.ContainerList(ctx, types.ContainerListOptions{
-			All:     true,
-			Filters: filters.NewArgs(filters.Arg("label", "com.supabase.cli.project="+utils.Config.ProjectId)),
-		})
-		if err != nil {
+		if err := utils.LoadConfigFS(fsys); err != nil {
 			return err
 		}
-
-		var wg sync.WaitGroup
-
-		for _, container := range containers {
-			wg.Add(1)
-
-			go func(containerId string) {
-				_ = utils.Docker.ContainerRemove(ctx, containerId, types.ContainerRemoveOptions{
-					RemoveVolumes: true,
-					Force:         true,
-				})
-
-				wg.Done()
-			}(container.ID)
+		if err := utils.AssertSupabaseDbIsRunning(); err != nil {
+			fmt.Println(utils.Aqua("supabase") + " local development setup is already stopped.")
+			return nil
 		}
-
-		wg.Wait()
 	}
 
-	// Remove networks.
-	if _, err := utils.Docker.NetworksPrune(
-		ctx,
-		filters.NewArgs(filters.Arg("label", "com.supabase.cli.project="+utils.Config.ProjectId)),
-	); err != nil {
+	// Stop all services
+	if err := stop(ctx); err != nil {
 		return err
 	}
 
-	// Remove temporary files.
-	if err := os.RemoveAll("supabase/.branches"); err != nil {
+	// Remove other branches
+	branchDir := filepath.Dir(utils.CurrBranchPath)
+	if err := fsys.RemoveAll(branchDir); err != nil {
 		return err
 	}
-
 	fmt.Println("Stopped " + utils.Aqua("supabase") + " local development setup.")
 
 	return nil
 }
+
+func stop(ctx context.Context) error {
+	args := filters.NewArgs(
+		filters.Arg("label", "com.supabase.cli.project="+utils.Config.ProjectId),
+	)
+	containers, err := utils.Docker.ContainerList(ctx, types.ContainerListOptions{
+		All:     true,
+		Filters: args,
+	})
+	if err != nil {
+		return err
+	}
+	// Remove containers.
+	ids := make([]string, len(containers))
+	for i, c := range containers {
+		ids[i] = c.ID
+	}
+	utils.DockerRemoveContainers(ctx, ids)
+	// Remove networks.
+	_, err = utils.Docker.NetworksPrune(ctx, args)
+	return err
+}
diff --git a/internal/utils/docker.go b/internal/utils/docker.go
index ae7708dd..9de44b66 100644
--- a/internal/utils/docker.go
+++ b/internal/utils/docker.go
@@ -91,7 +91,7 @@ func DockerRun(
 	return resp.Reader, nil
 }
 
-func DockerRemoveContainers(ctx context.Context) {
+func DockerRemoveContainers(ctx context.Context, containers []string) {
 	var wg sync.WaitGroup
 
 	for _, container := range containers {
@@ -115,7 +115,7 @@ func DockerRemoveContainers(ctx context.Context) {
 }
 
 func DockerRemoveAll(ctx context.Context, netId string) {
-	DockerRemoveContainers(ctx)
+	DockerRemoveContainers(ctx, containers)
 	_ = Docker.NetworkRemove(ctx, netId)
 }
 
diff --git a/internal/utils/misc.go b/internal/utils/misc.go
index 723ff59d..b1fc9975 100644
--- a/internal/utils/misc.go
+++ b/internal/utils/misc.go
@@ -108,10 +108,6 @@ func GetCurrentTimestamp() string {
 	return time.Now().UTC().Format("20060102150405")
 }
 
-func GetCurrentBranch() (string, error) {
-	return GetCurrentBranchFS(afero.NewOsFs())
-}
-
 func GetCurrentBranchFS(fsys afero.Fs) (string, error) {
 	branch, err := afero.ReadFile(fsys, CurrBranchPath)
 	if err != nil {
@@ -151,14 +147,6 @@ func NewError(s string) error {
 	return errors.New(s)
 }
 
-func AssertSupabaseStartIsRunning() error {
-	if err := LoadConfig(); err != nil {
-		return err
-	}
-
-	return AssertSupabaseDbIsRunning()
-}
-
 func AssertSupabaseDbIsRunning() error {
 	if _, err := Docker.ContainerInspect(context.Background(), DbId); err != nil {
 		return errors.New(Aqua("supabase start") + " is not running.")
@@ -232,10 +220,6 @@ func AssertSupabaseCliIsSetUpFS(fsys afero.Fs) error {
 	return nil
 }
 
-func AssertIsLinked() error {
-	return AssertIsLinkedFS(afero.NewOsFs())
-}
-
 func AssertIsLinkedFS(fsys afero.Fs) error {
 	if _, err := fsys.Stat(ProjectRefPath); errors.Is(err, os.ErrNotExist) {
 		return errors.New("Cannot find project ref. Have you run " + Aqua("supabase link") + "?")


Test patch:
diff --git a/internal/stop/stop_test.go b/internal/stop/stop_test.go
new file mode 100644
index 00000000..0432c212
--- /dev/null
+++ b/internal/stop/stop_test.go
@@ -0,0 +1,197 @@
+package stop
+
+import (
+	"context"
+	"net/http"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/client"
+	"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"
+	"gopkg.in/h2non/gock.v1"
+)
+
+func TestStopCommand(t *testing.T) {
+	const version = "1.41"
+
+	t.Run("stops containers", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		require.NoError(t, utils.WriteConfig(fsys, false))
+		require.NoError(t, afero.WriteFile(fsys, utils.CurrBranchPath, []byte("main"), 0644))
+		// Setup mock docker
+		require.NoError(t, client.WithHTTPClient(http.DefaultClient)(utils.Docker))
+		defer gock.OffAll()
+		gock.New("http:///var/run/docker.sock").
+			Head("/_ping").
+			Reply(http.StatusOK).
+			SetHeader("API-Version", version).
+			SetHeader("OSType", "linux")
+		gock.New("http:///var/run/docker.sock").
+			Get("/v" + version + "/containers").
+			Reply(http.StatusOK).
+			JSON(types.ContainerJSON{})
+		gock.New("http:///var/run/docker.sock").
+			Get("/v" + version + "/containers/json").
+			Reply(http.StatusOK).
+			JSON([]types.Container{})
+		gock.New("http:///var/run/docker.sock").
+			Post("/v" + version + "/networks/prune").
+			Reply(http.StatusOK).
+			JSON(types.NetworksPruneReport{})
+		// Run test
+		err := Run(context.Background(), fsys)
+		// Check error
+		assert.NoError(t, err)
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+		// Check branches removed
+		exists, err := afero.Exists(fsys, utils.CurrBranchPath)
+		assert.NoError(t, err)
+		assert.False(t, exists)
+	})
+
+	t.Run("throws error on invalid config", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Run test
+		err := Run(context.Background(), fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Missing config: open supabase/config.toml: file does not exist")
+	})
+
+	t.Run("ignores stopped database", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		require.NoError(t, utils.WriteConfig(fsys, false))
+		// Setup mock docker
+		require.NoError(t, client.WithHTTPClient(http.DefaultClient)(utils.Docker))
+		defer gock.OffAll()
+		gock.New("http:///var/run/docker.sock").
+			Head("/_ping").
+			Reply(http.StatusOK).
+			SetHeader("API-Version", version).
+			SetHeader("OSType", "linux")
+		gock.New("http:///var/run/docker.sock").
+			Get("/v" + version + "/containers").
+			Reply(http.StatusServiceUnavailable)
+		// Run test
+		err := Run(context.Background(), fsys)
+		// Check error
+		assert.NoError(t, err)
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	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, client.WithHTTPClient(http.DefaultClient)(utils.Docker))
+		defer gock.OffAll()
+		gock.New("http:///var/run/docker.sock").
+			Head("/_ping").
+			Reply(http.StatusOK).
+			SetHeader("API-Version", version).
+			SetHeader("OSType", "linux")
+		gock.New("http:///var/run/docker.sock").
+			Get("/v" + version + "/containers").
+			Reply(http.StatusOK).
+			JSON(types.ContainerJSON{})
+		gock.New("http:///var/run/docker.sock").
+			Get("/v" + version + "/containers/json").
+			Reply(http.StatusServiceUnavailable)
+		// Run test
+		err := Run(context.Background(), afero.NewReadOnlyFs(fsys))
+		// Check error
+		assert.ErrorContains(t, err, "request returned Service Unavailable for API route and version")
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	t.Run("throws error on permission denied", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		require.NoError(t, utils.WriteConfig(fsys, false))
+		// Setup mock docker
+		require.NoError(t, client.WithHTTPClient(http.DefaultClient)(utils.Docker))
+		defer gock.OffAll()
+		gock.New("http:///var/run/docker.sock").
+			Head("/_ping").
+			Reply(http.StatusOK).
+			SetHeader("API-Version", version).
+			SetHeader("OSType", "linux")
+		gock.New("http:///var/run/docker.sock").
+			Get("/v" + version + "/containers").
+			Reply(http.StatusOK).
+			JSON(types.ContainerJSON{})
+		gock.New("http:///var/run/docker.sock").
+			Get("/v" + version + "/containers/json").
+			Reply(http.StatusOK).
+			JSON([]types.Container{})
+		gock.New("http:///var/run/docker.sock").
+			Post("/v" + version + "/networks/prune").
+			Reply(http.StatusOK).
+			JSON(types.NetworksPruneReport{})
+		// Run test
+		err := Run(context.Background(), afero.NewReadOnlyFs(fsys))
+		// Check error
+		assert.ErrorContains(t, err, "operation not permitted")
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+}
+
+func TestStopServices(t *testing.T) {
+	const version = "1.41"
+
+	t.Run("stops all services", func(t *testing.T) {
+		containers := []types.Container{{ID: "c1"}, {ID: "c2"}}
+		// Setup mock docker
+		require.NoError(t, client.WithHTTPClient(http.DefaultClient)(utils.Docker))
+		defer gock.OffAll()
+		gock.New("http:///var/run/docker.sock").
+			Head("/_ping").
+			Reply(http.StatusOK).
+			SetHeader("API-Version", version).
+			SetHeader("OSType", "linux")
+		gock.New("http:///var/run/docker.sock").
+			Get("/v" + version + "/containers/json").
+			Reply(http.StatusOK).
+			JSON(containers)
+		for _, c := range containers {
+			gock.New("http:///var/run/docker.sock").
+				Delete("/v" + version + "/containers/" + c.ID).
+				Reply(http.StatusOK)
+		}
+		gock.New("http:///var/run/docker.sock").
+			Post("/v" + version + "/networks/prune").
+			Reply(http.StatusOK).
+			JSON(types.NetworksPruneReport{})
+		// Run test
+		err := stop(context.Background())
+		// Check error
+		assert.NoError(t, err)
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	t.Run("throws error on list failure", func(t *testing.T) {
+		// Setup mock docker
+		require.NoError(t, client.WithHTTPClient(http.DefaultClient)(utils.Docker))
+		defer gock.OffAll()
+		gock.New("http:///var/run/docker.sock").
+			Head("/_ping").
+			Reply(http.StatusOK).
+			SetHeader("API-Version", version).
+			SetHeader("OSType", "linux")
+		gock.New("http:///var/run/docker.sock").
+			Get("/v" + version + "/containers/json").
+			Reply(http.StatusServiceUnavailable)
+		// Run test
+		err := stop(context.Background())
+		// Check error
+		assert.ErrorContains(t, err, "request returned Service Unavailable for API route and version")
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+}