Repository: supabase/cli
Pull request #303 (2022-07-20 06:21:17)

Add unit tests for init command
## What kind of change does this PR introduce?

Unit tests with in-memory file system.

## What is the current behavior?

Config tests writes out to local filesystem.

## What is the new behavior?

- Uses afero as the abstract file system (`io/fs` only supports read operations, [read/write proposal](https://github.com/golang/go/issues/45757) is in progress)
- Simplifies config tests to use afero
- Adds unit test for init command

## Additional context

Add any other context or screenshots.



Patch:
diff --git a/cmd/init.go b/cmd/init.go
index f6e1433b..cdcf1e2b 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -3,6 +3,7 @@ package cmd
 import (
 	"fmt"
 
+	"github.com/spf13/afero"
 	"github.com/spf13/cobra"
 	_init "github.com/supabase/cli/internal/init"
 	"github.com/supabase/cli/internal/utils"
@@ -12,7 +13,8 @@ var initCmd = &cobra.Command{
 	Use:   "init",
 	Short: "Initialize a project to use Supabase CLI.",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		if err := _init.Run(); err != nil {
+		fsys := afero.NewOsFs()
+		if err := _init.Run(fsys); err != nil {
 			return err
 		}
 
diff --git a/go.mod b/go.mod
index 796341e7..3d336bcd 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,12 @@ require (
 	github.com/withfig/autocomplete-tools/packages/cobra v1.1.3
 )
 
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+
 require (
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 	github.com/Microsoft/go-winio v0.5.2 // indirect
@@ -60,10 +66,11 @@ require (
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/sirupsen/logrus v1.8.1 // indirect
-	github.com/spf13/afero v1.8.2 // indirect
+	github.com/spf13/afero v1.8.2
 	github.com/spf13/cast v1.4.1 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/stretchr/testify v1.8.0
 	github.com/subosito/gotenv v1.2.0 // indirect
 	github.com/yuin/goldmark v1.4.11 // indirect
 	github.com/yuin/goldmark-emoji v1.0.1 // indirect
diff --git a/go.sum b/go.sum
index af807654..7f615460 100644
--- a/go.sum
+++ b/go.sum
@@ -348,12 +348,15 @@ github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8q
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/withfig/autocomplete-tools/packages/cobra v1.1.3 h1:toi+jQYY7SG/DRYZxP2U2VkFeCY+6FYLXEr8QR8VXzY=
@@ -730,7 +733,8 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
 gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/internal/init/init.go b/internal/init/init.go
index fb3054e5..19e8c9a8 100644
--- a/internal/init/init.go
+++ b/internal/init/init.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"os"
 
+	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/utils"
 )
 
@@ -21,33 +22,33 @@ var (
 	errAlreadyInitialized = errors.New("Project already initialized. Remove " + utils.Bold("supabase") + " to reinitialize.")
 )
 
-func Run() error {
-	if err := run(); errors.Is(err, errAlreadyInitialized) {
+func Run(fsys afero.Fs) error {
+	if err := run(fsys); errors.Is(err, errAlreadyInitialized) {
 		return err
 	} else if err != nil {
-		_ = os.RemoveAll("supabase")
+		_ = fsys.RemoveAll("supabase")
 		return err
 	}
 
 	return nil
 }
 
-func run() error {
+func run(fsys afero.Fs) error {
 	// Sanity checks.
 	{
-		if _, err := os.ReadFile("supabase/config.toml"); err == nil {
+		if _, err := afero.ReadFile(fsys, "supabase/config.toml"); err == nil {
 			return errAlreadyInitialized
 		} else if !errors.Is(err, os.ErrNotExist) {
 			return err
 		}
 	}
 
-	if err := utils.MkdirIfNotExist("supabase"); err != nil {
+	if err := fsys.Mkdir("supabase", 0755); err != nil && !errors.Is(err, os.ErrExist) {
 		return err
 	}
 
 	// 1. Write `config.toml`.
-	if err := utils.WriteConfig(false); err != nil {
+	if err := utils.WriteConfig(fsys, false); err != nil {
 		return err
 	}
 
@@ -60,9 +61,9 @@ func run() error {
 			// skip
 		} else {
 			gitignorePath := *gitRoot + "/.gitignore"
-			gitignore, err := os.ReadFile(gitignorePath)
+			gitignore, err := afero.ReadFile(fsys, gitignorePath)
 			if errors.Is(err, os.ErrNotExist) {
-				if err := os.WriteFile(gitignorePath, initGitignore, 0644); err != nil {
+				if err := afero.WriteFile(fsys, gitignorePath, initGitignore, 0644); err != nil {
 					return err
 				}
 			} else if err != nil {
@@ -70,7 +71,7 @@ func run() error {
 			} else if bytes.Contains(gitignore, initGitignore) {
 				// skip
 			} else {
-				f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+				f, err := fsys.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
 				if err != nil {
 					return err
 				}
diff --git a/internal/utils/config.go b/internal/utils/config.go
index b42a97ec..83eb802e 100644
--- a/internal/utils/config.go
+++ b/internal/utils/config.go
@@ -12,6 +12,7 @@ import (
 	"text/template"
 
 	"github.com/BurntSushi/toml"
+	"github.com/spf13/afero"
 	"github.com/spf13/viper"
 )
 
@@ -125,7 +126,12 @@ type (
 )
 
 func LoadConfig() error {
-	if _, err := toml.DecodeFile("supabase/config.toml", &Config); err == nil {
+	return loadConfig(afero.NewOsFs())
+}
+
+func loadConfig(fsys afero.Fs) error {
+	// TODO: remove this helper once all sub commands pass in fsys
+	if _, err := toml.DecodeFS(afero.NewIOFS(fsys), "supabase/config.toml", &Config); err == nil {
 		// skip
 	} else if errors.Is(err, os.ErrNotExist) {
 		_, _err := os.Stat("supabase/config.json")
@@ -388,7 +394,8 @@ secret = ""
 	return nil
 }
 
-func WriteConfig(test bool) error {
+func WriteConfig(fsys afero.Fs, test bool) error {
+	// Using current directory name as project id
 	cwd, err := os.Getwd()
 	if err != nil {
 		return err
@@ -410,7 +417,7 @@ func WriteConfig(test bool) error {
 		return err
 	}
 
-	if err := os.WriteFile("supabase/config.toml", initConfigBuf.Bytes(), 0644); err != nil {
+	if err := afero.WriteFile(fsys, "supabase/config.toml", initConfigBuf.Bytes(), 0644); err != nil {
 		return err
 	}
 


Test patch:
diff --git a/internal/init/init_test.go b/internal/init/init_test.go
new file mode 100644
index 00000000..c3a873e3
--- /dev/null
+++ b/internal/init/init_test.go
@@ -0,0 +1,29 @@
+package init
+
+import (
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestInitCommand(t *testing.T) {
+	t.Run("creates config file", func(t *testing.T) {
+		fsys := &afero.MemMapFs{}
+		assert.NoError(t, Run(fsys))
+		// TODO: verify .gitignore
+		exists, err := afero.Exists(fsys, "supabase/config.toml")
+		assert.NoError(t, err)
+		assert.True(t, exists)
+	})
+
+	t.Run("errors when config file exists", func(t *testing.T) {
+		// Test setup
+		fsys := &afero.MemMapFs{}
+		_, err := fsys.Create("supabase/config.toml")
+		require.NoError(t, err)
+		// Actual test
+		assert.Error(t, Run(fsys))
+	})
+}
diff --git a/internal/utils/config_test.go b/internal/utils/config_test.go
index 11fe3477..77227b45 100644
--- a/internal/utils/config_test.go
+++ b/internal/utils/config_test.go
@@ -1,83 +1,36 @@
 package utils
 
 import (
-	"os"
 	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestConfigParsing(t *testing.T) {
-	t.Cleanup(func() {
-		if err := os.Remove("supabase/config.toml"); err != nil {
-			if !os.IsNotExist(err) {
-				t.Error(err)
-			}
-		}
-		if err := os.Remove("supabase"); err != nil {
-			if !os.IsNotExist(err) {
-				t.Error(err)
-			}
-		}
-	})
-
-	if err := os.Mkdir("supabase", 0755); err != nil {
-		t.Error(err)
-		t.FailNow()
-	}
-
 	t.Run("classic config file", func(t *testing.T) {
-		if err := WriteConfig(false); err != nil {
-			t.Error(err)
-			t.FailNow()
-		}
-		if err := LoadConfig(); err != nil {
-			t.Error(err)
-			t.FailNow()
-		}
+		fsys := afero.NewMemMapFs()
+		assert.NoError(t, WriteConfig(fsys, false))
+		assert.NoError(t, loadConfig(fsys))
 	})
 
 	t.Run("config file with environment variables", func(t *testing.T) {
-		if err := WriteConfig(true); err != nil {
-			t.Error(err)
-			t.FailNow()
-		}
+		fsys := afero.NewMemMapFs()
+		assert.NoError(t, WriteConfig(fsys, true))
 
 		t.Setenv("AZURE_CLIENT_ID", "hello")
 		t.Setenv("AZURE_SECRET", "this is cool")
-		if err := LoadConfig(); err != nil {
-			t.Error(err)
-			t.FailNow()
-		}
-		if err := InterpolateEnvInConfig(); err != nil {
-			t.Error(err)
-			t.FailNow()
-		}
-		t.Setenv("AZURE_CLIENT_ID", "hello")
-		t.Setenv("AZURE_SECRET", "this is cool")
+		assert.NoError(t, loadConfig(fsys))
+		assert.NoError(t, InterpolateEnvInConfig())
 
-		if Config.Auth.External["azure"].ClientId != "hello" {
-			t.Errorf("unexpected value for key [ClientId]: %+v", Config.Auth.External["azure"])
-			t.FailNow()
-		}
-
-		if Config.Auth.External["azure"].Secret != "this is cool" {
-			t.Errorf("unexpected value for key [Secret]: %+v", Config.Auth.External["azure"])
-			t.FailNow()
-		}
+		assert.Equal(t, "hello", Config.Auth.External["azure"].ClientId)
+		assert.Equal(t, "this is cool", Config.Auth.External["azure"].Secret)
 	})
 
 	t.Run("config file with environment variables fails when unset", func(t *testing.T) {
-		if err := WriteConfig(true); err != nil {
-			t.Error(err)
-			t.FailNow()
-		}
-
-		if err := LoadConfig(); err != nil {
-			t.Error(err)
-			t.FailNow()
-		}
-		if err := InterpolateEnvInConfig(); err == nil {
-			t.Error("expected to fail")
-			t.FailNow()
-		}
+		fsys := afero.NewMemMapFs()
+		assert.NoError(t, WriteConfig(fsys, true))
+		assert.NoError(t, loadConfig(fsys))
+		assert.Error(t, InterpolateEnvInConfig())
 	})
 }