Repository: supabase/cli
Pull request #283 (2022-06-21 16:39:17)

feat: add tests and simple docker mock
## What kind of change does this PR introduce?

adding tests for cli

soedirgo: What do you think about doing E2E tests to start with, instead of integration tests with mocks?

A lot of the issues we (& users) have faced wouldn't surface without real Docker containers. I'm worried about using test doubles because it introduces maintenance burden, and this is problematic for this repo especially since we have bus factor == 1. Also, I'm expecting some major refactors in the future, so I don't want to risk having to redo the test setup.

I kind of lapsed on setting up proper testing here, but roughly my idea is to start with E2E testing centered on regressions (I can comb through old issues and include these on our internal manual testing board), and once we have a stable architecture in place, start doing more fine-grained testing.
egor-romanov: > What do you think about doing E2E tests to start with, instead of integration tests with mocks?

Hey, yeah I was thinking about e2e testing, but as we have intensive docker API usage in cli, it can really be the source of very long running tests (since it is required to pull all the images, wait till it running, applying migration, etc). But what is even more dangerous - is that it can be the source of high flakiness.

Instead with the current setup all tests may be run in fractions of a second. And there should be no flakiness at all.

Also these are not unit tests, cause everything is mocked on API level, we are mocking `docker daemon` with real tcp server and providing another docker host url to the CLI and also `supabase API` is mocked as the real http server. Where we can control if we would like to return error or make the request "stuck" for example.

Also all CLI calls are made with constructions like `init, _, err := suite.cmd.Find([]string{"init"})` ` init.RunE(init, []string{})` which almost completely mimics the real CLI calls. And should be no difference between running it like e2e or integration tests.

> A lot of the issues we (& users) have faced wouldn't surface without real Docker containers. I'm worried about using test doubles because it introduces maintenance burden, and this is problematic for this repo especially since we have bus factor == 1. Also, I'm expecting some major refactors in the future, so I don't want to risk having to redo the test setup.

Not really very worried here from the view of bus factor and everything, cause e2e is less explanatory from this point of view. With current testing I am adding [extensive checks](https://github.com/supabase/cli/pull/283/files#diff-2f1494c5f844cb6bb73d2a3a37a56ae7d9ca319ca60390cd30b9e689ad2d3d08R23-R37) on what exact calls were made to the docker API and/or Supabase API. Which btw spoiled some issues as for me, for example we are checking only if db container is running in status checks.

Of course it is required that you check at least once that the new feature is working: to be sure that your `docker exec` commands are doing what it is supposed to do.

> I kind of lapsed on setting up proper testing here, but roughly my idea is to start with E2E testing centered on regressions (I can comb through old issues and include these on our internal manual testing board), and once we have a stable architecture in place, start doing more fine-grained testing.

I am not so sure in that rule btw, if we have a lot of reoccurring bugs that repeat from time to time on releases, then of course these cases have to be automated. But in most cases with correct git flow and everything it shouldn't be the issue in the modern times. Like what can be the reason to add the same bug again and again (especially when there is only one maintainer)?

We should add some e2e tests after all for the most used user scenarios. But as this app is not very complicated and huge, there won't be a lot of them. For example "start the local infra".
egor-romanov: Yeah, jic I ve already been in the position of testing go projects, cli included, and I was referring to "how google does it" and their kubectl for example in this case
soedirgo: Hmm ok, I've got to admit I'm new to testing in Go - for now let's enable these tests but not block merges? Then once we have more confidence we can require passing checks on PRs.
egor-romanov: @sweatybridge after some of your latest changes total coverage dropped by 10%

it was 27% on my last commit and now 16%
egor-romanov: https://github.com/supabase/cli/runs/7462491975?check_suite_focus=true#step:4:263

https://github.com/supabase/cli/runs/7455234657?check_suite_focus=true#step:4:267
sweatybridge: > @sweatybridge after some of your latest changes total coverage dropped by 10% it was 27% on my last commit and now 14.6%

@egor-romanov  I believe the drop is due to inclusion of more files, such as `integration/mock`. The flag we need to tweak is `-coverpkg ./...` which adds all files to coverage.

Previously 27% was from unit test only, not including integration tests. You can verify it by checking out the current `main` branch and run the following script.

```bash
go test ./... -coverprofile=coverage.out
go tool cover -func coverage.out
```
egor-romanov: Hey, yep, I’ve checked commits and found this, we can add except for test package 

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 71bd8e77..4feff536 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,6 +2,8 @@ name: CI
 
 on:
   pull_request:
+    branches:
+      - main
   workflow_dispatch:
 
 jobs:
@@ -17,7 +19,9 @@ jobs:
 
       - run: |
           if [ ! -z $(gofmt -l .) ]; then echo 'Make sure to run "go fmt ./..." before commit!' && exit 1; fi
-          go test ./...
+          mkdir  /home/runner/.supabase && chown -R runner /home/runner/.supabase
+          go test ./... -race -v -count=1 -coverpkg ./cmd/...,./internal/...,./pkg/... -coverprofile=coverage.out
+          go tool cover -func coverage.out | grep total
 
   lint:
     name: Lint
@@ -31,4 +35,4 @@ jobs:
 
       - uses: golangci/golangci-lint-action@v3
         with:
-          version: v1.46
+          version: v1.47
diff --git a/.gitignore b/.gitignore
index cf93d0e9..fce8fcdf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,5 +14,9 @@
 # Dependency directories (remove the comment below to include it)
 # vendor/
 
+# IDE
+.vscode
+
 # Supabase
-/supabase
+**/supabase/.branches
+**/supabase/.temp
diff --git a/cmd/root.go b/cmd/root.go
index 1886b0df..e288bfd4 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -22,3 +22,9 @@ func Execute() {
 func init() {
 	rootCmd.SetVersionTemplate("{{.Version}}\n")
 }
+
+// instantiate new rootCmd is a bit tricky with cobra, but it can be done later with the following
+// approach for example: https://github.com/portworx/pxc/tree/master/cmd
+func GetRootCmd() *cobra.Command {
+	return rootCmd
+}
diff --git a/go.mod b/go.mod
index ba7efb8c..fed65af9 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
 	github.com/docker/go-connections v0.4.0
 	github.com/jackc/pgx/v4 v4.15.0
 	github.com/joho/godotenv v1.4.0
+	github.com/matoous/go-nanoid/v2 v2.0.0
 	github.com/mattn/go-isatty v0.0.14
 	github.com/muesli/reflow v0.3.0
 	github.com/spf13/cobra v1.4.0
@@ -21,8 +22,19 @@ require (
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-playground/locales v0.14.0 // indirect
+	github.com/go-playground/universal-translator v0.18.0 // indirect
+	github.com/go-playground/validator/v10 v10.10.0 // indirect
+	github.com/goccy/go-json v0.9.7 // indirect
 	github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/leodido/go-urn v1.2.1 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/ugorji/go/codec v1.2.7 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
@@ -38,6 +50,7 @@ require (
 	github.com/docker/distribution v2.8.1+incompatible // indirect
 	github.com/docker/go-units v0.4.0 // indirect
 	github.com/fsnotify/fsnotify v1.5.1 // indirect
+	github.com/gin-gonic/gin v1.8.1
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/gorilla/css v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 8804c18e..56b2f848 100644
--- a/go.sum
+++ b/go.sum
@@ -87,6 +87,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
 github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -112,12 +113,26 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
 github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
+github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
+github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
+github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
+github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
+github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
+github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
+github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
 github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -163,6 +178,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -245,6 +261,8 @@ github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
 github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
 github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -252,13 +270,18 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -268,6 +291,9 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
 github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
+github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
+github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@@ -288,6 +314,11 @@ github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGg
 github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
 github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
@@ -300,6 +331,7 @@ github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtl
 github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
 github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
 github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
@@ -309,6 +341,9 @@ github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrB
 github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
 github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
 github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
+github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -321,6 +356,9 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
 github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
 github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
 github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
@@ -356,12 +394,16 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 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/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
+github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
+github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
 github.com/withfig/autocomplete-tools/packages/cobra v1.1.3 h1:toi+jQYY7SG/DRYZxP2U2VkFeCY+6FYLXEr8QR8VXzY=
 github.com/withfig/autocomplete-tools/packages/cobra v1.1.3/go.mod h1:RoXh7+7qknOXL65uTzdzE1mPxqcPwS7FLCE9K5GfmKo=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -541,6 +583,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
 golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -725,8 +768,9 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
 google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
 gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
@@ -738,6 +782,7 @@ 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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 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=
diff --git a/internal/functions/delete/delete.go b/internal/functions/delete/delete.go
index 6f43dc5a..12cedc5c 100644
--- a/internal/functions/delete/delete.go
+++ b/internal/functions/delete/delete.go
@@ -59,7 +59,11 @@ func Run(slug string, projectRefArg string) error {
 			return err
 		}
 
-		req, err := http.NewRequest("GET", "https://api.supabase.io/v1/projects/"+projectRef+"/functions/"+slug, nil)
+		supabaseAPI := os.Getenv("SUPABASE_INTERNAL_API_HOST")
+		if supabaseAPI == "" {
+			supabaseAPI = "https://api.supabase.io"
+		}
+		req, err := http.NewRequest("GET", supabaseAPI+"/v1/projects/"+projectRef+"/functions/"+slug, nil)
 		if err != nil {
 			return err
 		}
@@ -74,7 +78,7 @@ func Run(slug string, projectRefArg string) error {
 		case http.StatusNotFound: // Function doesn't exist
 			return errors.New("Function " + utils.Aqua(slug) + " does not exist on the Supabase project.")
 		case http.StatusOK: // Function exists
-			req, err := http.NewRequest("DELETE", "https://api.supabase.io/v1/projects/"+projectRef+"/functions/"+slug, nil)
+			req, err := http.NewRequest("DELETE", supabaseAPI+"/v1/projects/"+projectRef+"/functions/"+slug, nil)
 			if err != nil {
 				return err
 			}
diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go
index ec32638f..43214fd4 100644
--- a/internal/functions/deploy/deploy.go
+++ b/internal/functions/deploy/deploy.go
@@ -122,7 +122,11 @@ Enter your project ref: `)
 			return err
 		}
 
-		req, err := http.NewRequest("GET", "https://api.supabase.io/v1/projects/"+projectRef+"/functions/"+slug, nil)
+		supabaseAPI := os.Getenv("SUPABASE_INTERNAL_API_HOST")
+		if supabaseAPI == "" {
+			supabaseAPI = "https://api.supabase.io"
+		}
+		req, err := http.NewRequest("GET", supabaseAPI+"/v1/projects/"+projectRef+"/functions/"+slug, nil)
 		if err != nil {
 			return err
 		}
@@ -135,12 +139,19 @@ Enter your project ref: `)
 
 		switch resp.StatusCode {
 		case http.StatusNotFound: // Function doesn't exist yet, so do a POST
-			jsonBytes, err := json.Marshal(map[string]interface{}{"slug": slug, "name": slug, "body": newFunctionBody, "verify_jwt": verifyJWT})
+			jsonBytes, err := json.Marshal(
+				map[string]interface{}{
+					"slug":       slug,
+					"name":       slug,
+					"body":       newFunctionBody,
+					"verify_jwt": verifyJWT,
+				})
 			if err != nil {
 				return err
 			}
 
-			req, err := http.NewRequest("POST", "https://api.supabase.io/v1/projects/"+projectRef+"/functions", bytes.NewReader(jsonBytes))
+			req, err := http.NewRequest(
+				"POST", supabaseAPI+"/v1/projects/"+projectRef+"/functions", bytes.NewReader(jsonBytes))
 			if err != nil {
 				return err
 			}
@@ -168,7 +179,8 @@ Enter your project ref: `)
 				return err
 			}
 
-			req, err := http.NewRequest("PATCH", "https://api.supabase.io/v1/projects/"+projectRef+"/functions/"+slug, bytes.NewReader(jsonBytes))
+			req, err := http.NewRequest(
+				"PATCH", supabaseAPI+"/v1/projects/"+projectRef+"/functions/"+slug, bytes.NewReader(jsonBytes))
 			if err != nil {
 				return err
 			}
@@ -200,7 +212,11 @@ Enter your project ref: `)
 
 	fmt.Println("Deployed Function " + utils.Aqua(slug) + " on project " + utils.Aqua(projectRef))
 
-	url := fmt.Sprintf("https://app.supabase.io/project/%v/functions/%v/details", projectRef, data.Id)
+	supabaseAPI := os.Getenv("SUPABASE_INTERNAL_API_HOST")
+	if supabaseAPI == "" {
+		supabaseAPI = "https://api.supabase.io"
+	}
+	url := fmt.Sprintf("%s/project/%v/functions/%v/details", supabaseAPI, projectRef, data.Id)
 	fmt.Println("You can inspect your deployment in the Dashboard: " + url)
 
 	return nil
diff --git a/internal/link/link.go b/internal/link/link.go
index c56c93a3..3b6d8d0c 100644
--- a/internal/link/link.go
+++ b/internal/link/link.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"os"
 
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/utils"
@@ -22,7 +23,11 @@ func Run(projectRef string, fsys afero.Fs) error {
 			return err
 		}
 
-		req, err := http.NewRequest("GET", "https://api.supabase.io/v1/projects/"+projectRef+"/functions", nil)
+		supabaseAPI := os.Getenv("SUPABASE_INTERNAL_API_HOST")
+		if supabaseAPI == "" {
+			supabaseAPI = "https://api.supabase.io"
+		}
+		req, err := http.NewRequest("GET", supabaseAPI+"/v1/projects/"+projectRef+"/functions", nil)
 		if err != nil {
 			return err
 		}
diff --git a/internal/secrets/list/list.go b/internal/secrets/list/list.go
index ba5717ff..d9071b47 100644
--- a/internal/secrets/list/list.go
+++ b/internal/secrets/list/list.go
@@ -38,7 +38,11 @@ func Run() error {
 			return err
 		}
 
-		req, err := http.NewRequest("GET", "https://api.supabase.io/v1/projects/"+projectRef+"/secrets", nil)
+		supabaseAPI := os.Getenv("SUPABASE_INTERNAL_API_HOST")
+		if supabaseAPI == "" {
+			supabaseAPI = "https://api.supabase.io"
+		}
+		req, err := http.NewRequest("GET", supabaseAPI+"/v1/projects/"+projectRef+"/secrets", nil)
 		if err != nil {
 			return err
 		}
diff --git a/internal/secrets/set/set.go b/internal/secrets/set/set.go
index 172d1a61..7ca3ec15 100644
--- a/internal/secrets/set/set.go
+++ b/internal/secrets/set/set.go
@@ -79,7 +79,11 @@ func Run(envFilePath string, args []string) error {
 		}
 		reqBody := bytes.NewReader(secretsBytes)
 
-		req, err := http.NewRequest("POST", "https://api.supabase.io/v1/projects/"+projectRef+"/secrets", reqBody)
+		supabaseAPI := os.Getenv("SUPABASE_INTERNAL_API_HOST")
+		if supabaseAPI == "" {
+			supabaseAPI = "https://api.supabase.io"
+		}
+		req, err := http.NewRequest("POST", supabaseAPI+"/v1/projects/"+projectRef+"/secrets", reqBody)
 		if err != nil {
 			return err
 		}
diff --git a/internal/secrets/unset/unset.go b/internal/secrets/unset/unset.go
index 08680bf5..463ab853 100644
--- a/internal/secrets/unset/unset.go
+++ b/internal/secrets/unset/unset.go
@@ -42,7 +42,12 @@ func Run(args []string) error {
 		}
 		reqBody := bytes.NewReader(secretsNamesBytes)
 
-		req, err := http.NewRequest("DELETE", "https://api.supabase.io/v1/projects/"+projectRef+"/secrets", reqBody)
+		supabaseAPI := os.Getenv("SUPABASE_INTERNAL_API_HOST")
+		if supabaseAPI == "" {
+			supabaseAPI = "https://api.supabase.io"
+		}
+		req, err := http.NewRequest(
+			"DELETE", supabaseAPI+"/v1/projects/"+projectRef+"/secrets", reqBody)
 		if err != nil {
 			return err
 		}
diff --git a/internal/utils/docker.go b/internal/utils/docker.go
index cf0034f3..3a5d462d 100644
--- a/internal/utils/docker.go
+++ b/internal/utils/docker.go
@@ -15,13 +15,11 @@ import (
 )
 
 var Docker = func() *client.Client {
-	docker, err := client.NewClientWithOpts(client.WithAPIVersionNegotiation())
-	if err != nil {
-		fmt.Fprintln(os.Stderr, "Failed to initialize Docker client:", err)
-		os.Exit(1)
-	}
-	// Support env (e.g. for mock setup or rootless docker)
-	err = client.FromEnv(docker)
+	docker, err := client.NewClientWithOpts(
+		client.WithAPIVersionNegotiation(),
+		// Support env (e.g. for mock setup or rootless docker)
+		client.FromEnv,
+	)
 	if err != nil {
 		fmt.Fprintln(os.Stderr, "Failed to initialize Docker client:", err)
 		os.Exit(1)


Test patch:
diff --git a/test/branch_test.go b/test/branch_test.go
new file mode 100644
index 00000000..1d01fdf7
--- /dev/null
+++ b/test/branch_test.go
@@ -0,0 +1,38 @@
+package integration
+
+import (
+	"os"
+
+	"github.com/stretchr/testify/require"
+)
+
+// this is the part of Database test suite - DBTestSuite
+// test functions
+func (suite *DBTestSuite) TestBranchCreate() {
+	// create branch
+	branch := "test-branch"
+	create, args, err := suite.cmd.Traverse([]string{"db", "branch", "create", branch})
+	if err != nil {
+		suite.Fail("failed to find create command")
+	}
+	err = create.RunE(create, args)
+	if err != nil {
+		suite.Fail("failed to create branch", err)
+	}
+
+	// check if branch dir exists
+	_, err = os.Stat("supabase/.branches/" + branch)
+	require.NoError(suite.T(), err)
+
+	// check if all exec calls were made to docker api
+	ids := suite.constructParams()
+	require.ElementsMatch(suite.T(), suite.params, ids)
+
+	// check commands in exec calls
+	require.ElementsMatch(suite.T(), suite.bodies, []string{
+		"{\"User\":\"\",\"Privileged\":false,\"Tty\":false,\"AttachStdin\":false,\"AttachStderr\":true,\"AttachStdout\":true,\"Detach\":false,\"DetachKeys\":\"\",\"Env\":null,\"WorkingDir\":\"\",\"Cmd\":[\"pg_dump\",\"postgresql://postgres:postgres@localhost/postgres\"]}\n",
+		"{\"Detach\":false,\"Tty\":false}\n",
+		"{\"User\":\"\",\"Privileged\":false,\"Tty\":false,\"AttachStdin\":false,\"AttachStderr\":true,\"AttachStdout\":true,\"Detach\":false,\"DetachKeys\":\"\",\"Env\":null,\"WorkingDir\":\"\",\"Cmd\":[\"sh\",\"-c\",\"psql --set ON_ERROR_STOP=on postgresql://postgres:postgres@localhost/postgres \\u003c\\u003c'EOSQL'\\nCREATE DATABASE \\\"" + branch + "\\\";\\n\\\\connect " + branch + "\\nBEGIN;\\nexit code 0\\nCOMMIT;\\nEOSQL\\n\"]}\n",
+		"{\"Detach\":false,\"Tty\":false}\n",
+	})
+}
diff --git a/test/db_test.go b/test/db_test.go
new file mode 100644
index 00000000..4fd35610
--- /dev/null
+++ b/test/db_test.go
@@ -0,0 +1,148 @@
+package integration
+
+// Basic imports
+import (
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+	"sync"
+	"testing"
+
+	"github.com/gin-gonic/gin"
+	gonanoid "github.com/matoous/go-nanoid/v2"
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	clicmd "github.com/supabase/cli/cmd"
+	"github.com/supabase/cli/test/mocks/docker"
+)
+
+type DBTestSuite struct {
+	suite.Suite
+	cmd     *cobra.Command
+	tempDir string
+	ids     []string
+	bodies  []string
+	params  []gin.Params
+	mtx     sync.RWMutex
+}
+
+// test functions
+// add tests here <-
+
+// hooks
+func (suite *DBTestSuite) SetupTest() {
+	suite.tempDir = NewTempDir(Logger, TempDir)
+	suite.mtx.Lock()
+	suite.ids = []string{}
+	suite.bodies = []string{}
+	suite.params = []gin.Params{}
+	suite.mtx.Unlock()
+
+	// add docker mock handlers
+	DockerMock.ExecCreateHandler = func(c *gin.Context) {
+		suite.addParams(c.Copy())
+		body, err := ioutil.ReadAll(c.Request.Body)
+		if err != nil {
+			c.JSON(http.StatusBadRequest, gin.H{
+				"message": "error reading body",
+			})
+			return
+		}
+		suite.addBody(c, body)
+
+		id := gonanoid.MustGenerate(docker.IDAlphabet, docker.IDLength)
+		c.JSON(http.StatusCreated, gin.H{
+			"Id": id,
+		})
+		suite.addID(c, id)
+	}
+
+	DockerMock.ExecStartHandler = func(c *gin.Context) {
+		suite.addParams(c.Copy())
+		body, err := ioutil.ReadAll(c.Request.Body)
+		if err != nil {
+			c.JSON(http.StatusBadRequest, gin.H{
+				"message": "error reading body",
+			})
+			return
+		}
+		suite.addBody(c, body)
+
+		docker.HijackedResponse(c, "0")
+	}
+
+	DockerMock.ContainerInspectHandler = func(c *gin.Context) {
+		c.JSON(http.StatusOK, gin.H{})
+	}
+
+	// create supabase dir
+	suite.cmd = clicmd.GetRootCmd()
+	init, _, err := suite.cmd.Find([]string{"init"})
+	if err != nil {
+		suite.Fail("failed to find init command")
+	}
+	err = init.RunE(init, []string{})
+	if err != nil {
+		suite.Fail("failed to init supabase cli")
+	}
+
+	err = os.Mkdir("supabase/.branches", os.FileMode(0755))
+	if err != nil {
+		suite.Fail("failed to create supabase/.branches directory")
+	}
+}
+
+func (suite *DBTestSuite) TeardownTest() {
+	require.NoError(suite.T(), os.Chdir(TempDir))
+}
+
+// In order for 'go test' to run this suite, we need to create
+// a normal test function and pass our suite to suite.Run
+func TestDBTestSuite(t *testing.T) {
+	suite.Run(t, new(DBTestSuite))
+}
+
+// helper functions
+func (suite *DBTestSuite) addParams(c *gin.Context) {
+	suite.mtx.Lock()
+	defer suite.mtx.Unlock()
+	suite.params = append(suite.params, c.Params)
+}
+
+func (suite *DBTestSuite) addBody(c *gin.Context, body []byte) {
+	suite.mtx.Lock()
+	defer suite.mtx.Unlock()
+	suite.bodies = append(suite.bodies, string(body))
+}
+
+func (suite *DBTestSuite) addID(c *gin.Context, id string) {
+	suite.mtx.Lock()
+	defer suite.mtx.Unlock()
+	suite.ids = append(suite.ids, id)
+}
+
+func (suite *DBTestSuite) constructParams() []gin.Params {
+	ids := []gin.Params{}
+	// for each exec docker call we have to calls to docker api:
+	// one to create exec, one to start exec
+	for _, id := range suite.ids {
+		// this one represents call to create exec
+		ids = append(ids, gin.Params{
+			gin.Param{
+				Key:   "id",
+				Value: "supabase_db_" + filepath.Base(suite.tempDir),
+			},
+		})
+
+		// this one represents call to start exec
+		ids = append(ids, gin.Params{
+			gin.Param{
+				Key:   "id",
+				Value: id,
+			},
+		})
+	}
+	return ids
+}
diff --git a/test/init_test.go b/test/init_test.go
new file mode 100644
index 00000000..2d439c17
--- /dev/null
+++ b/test/init_test.go
@@ -0,0 +1,47 @@
+package integration
+
+// Basic imports
+import (
+	"os"
+	"testing"
+
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	clicmd "github.com/supabase/cli/cmd"
+)
+
+type InitTestSuite struct {
+	suite.Suite
+	tempDir string
+	cmd     *cobra.Command
+}
+
+// test functions
+func (suite *InitTestSuite) TestInit() {
+	// init supabase
+	init, _, err := suite.cmd.Find([]string{"init"})
+	require.NoError(suite.T(), err)
+	require.NoError(suite.T(), init.RunE(init, []string{}))
+
+	// check if init dir exists
+	_, err = os.Stat("supabase/config.toml")
+	require.NoError(suite.T(), err)
+}
+
+// hooks
+func (suite *InitTestSuite) SetupTest() {
+	// init cli
+	suite.cmd = clicmd.GetRootCmd()
+	suite.tempDir = NewTempDir(Logger, TempDir)
+}
+
+func (suite *InitTestSuite) TeardownTest() {
+	require.NoError(suite.T(), os.Chdir(TempDir))
+}
+
+// In order for 'go test' to run this suite, we need to create
+// a normal test function and pass our suite to suite.Run
+func TestInitTestSuite(t *testing.T) {
+	suite.Run(t, new(InitTestSuite))
+}
diff --git a/test/link_test.go b/test/link_test.go
new file mode 100644
index 00000000..cc712883
--- /dev/null
+++ b/test/link_test.go
@@ -0,0 +1,100 @@
+package integration
+
+// Basic imports
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"sync"
+	"testing"
+
+	"github.com/gin-gonic/gin"
+	gonanoid "github.com/matoous/go-nanoid/v2"
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	clicmd "github.com/supabase/cli/cmd"
+	"github.com/supabase/cli/test/mocks/supabase"
+)
+
+type LinkTestSuite struct {
+	suite.Suite
+	tempDir string
+	cmd     *cobra.Command
+
+	ids     []string
+	headers []http.Header
+
+	mtx sync.RWMutex
+}
+
+// test functions
+func (suite *LinkTestSuite) TestLink() {
+	// run command
+	link, _, err := suite.cmd.Find([]string{"link"})
+	require.NoError(suite.T(), err)
+	key := "sbp_" + gonanoid.MustGenerate(supabase.KeyAlphabet, supabase.KeyLength)
+	os.Setenv("SUPABASE_ACCESS_TOKEN", key)
+	id := gonanoid.MustGenerate(supabase.IDAlphabet, supabase.IDLength)
+	require.NoError(suite.T(), link.Flags().Set("project-ref", id))
+	require.NoError(suite.T(), link.RunE(link, []string{}))
+
+	// check request details
+	suite.mtx.RLock()
+	defer suite.mtx.RUnlock()
+	require.Contains(suite.T(), suite.ids, id)
+	require.Contains(suite.T(), suite.headers, http.Header{
+		"Authorization":   []string{fmt.Sprintf("Bearer %s", key)},
+		"Accept-Encoding": []string{"gzip"},
+		"User-Agent":      []string{"Go-http-client/1.1"},
+	})
+	_, err = os.Stat("supabase/.temp/project-ref")
+	require.NoError(suite.T(), err)
+	ref, err := ioutil.ReadFile("supabase/.temp/project-ref")
+	require.NoError(suite.T(), err)
+	require.Equal(suite.T(), id, string(ref))
+}
+
+// hooks
+func (suite *LinkTestSuite) SetupTest() {
+	// init cli
+	suite.cmd = clicmd.GetRootCmd()
+	suite.tempDir = NewTempDir(Logger, TempDir)
+
+	// init supabase
+	init, _, err := suite.cmd.Find([]string{"init"})
+	require.NoError(suite.T(), err)
+	require.NoError(suite.T(), init.RunE(init, []string{}))
+
+	// implement mocks
+	SupaMock.FunctionsHandler = func(c *gin.Context) {
+		suite.addHeaders(c.Request.Header)
+		suite.addID(c.Params.ByName("id"))
+
+		c.JSON(http.StatusOK, gin.H{})
+	}
+}
+
+func (suite *LinkTestSuite) TeardownTest() {
+	require.NoError(suite.T(), os.Chdir(TempDir))
+}
+
+// In order for 'go test' to run this suite, we need to create
+// a normal test function and pass our suite to suite.Run
+func TestLinkTestSuite(t *testing.T) {
+	suite.Run(t, new(LinkTestSuite))
+}
+
+// helper functions
+func (suite *LinkTestSuite) addID(id string) {
+	suite.mtx.Lock()
+	defer suite.mtx.Unlock()
+	suite.ids = append(suite.ids, id)
+}
+
+func (suite *LinkTestSuite) addHeaders(headers http.Header) {
+	suite.mtx.Lock()
+	defer suite.mtx.Unlock()
+	suite.headers = append(suite.headers, headers)
+}
diff --git a/test/login_test.go b/test/login_test.go
new file mode 100644
index 00000000..f5130174
--- /dev/null
+++ b/test/login_test.go
@@ -0,0 +1,107 @@
+package integration
+
+// Basic imports
+import (
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+	"sync"
+	"testing"
+
+	"github.com/gin-gonic/gin"
+	gonanoid "github.com/matoous/go-nanoid/v2"
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	clicmd "github.com/supabase/cli/cmd"
+	"github.com/supabase/cli/test/mocks/supabase"
+)
+
+type LoginTestSuite struct {
+	suite.Suite
+	tempDir string
+	cmd     *cobra.Command
+
+	ids     []string
+	headers []http.Header
+
+	mtx sync.RWMutex
+}
+
+// test functions
+func (suite *LoginTestSuite) TestLink() {
+	// run command
+	login, _, err := suite.cmd.Find([]string{"login"})
+	require.NoError(suite.T(), err)
+	key := "sbp_" + gonanoid.MustGenerate(supabase.KeyAlphabet, supabase.KeyLength)
+
+	// change stdin to read from a file
+	content := []byte(key)
+	tmpfile, err := ioutil.TempFile(suite.tempDir, "key")
+	require.NoError(suite.T(), err)
+	defer os.Remove(tmpfile.Name()) // clean up
+
+	_, err = tmpfile.Write(content)
+	require.NoError(suite.T(), err)
+	_, err = tmpfile.Seek(0, 0)
+	require.NoError(suite.T(), err)
+
+	oldStdin := os.Stdin
+	defer func() { os.Stdin = oldStdin }()
+	os.Stdin = tmpfile
+
+	require.NoError(suite.T(), login.RunE(login, []string{}))
+
+	// check token is saved
+	home, err := os.UserHomeDir()
+	require.NoError(suite.T(), err)
+	_, err = os.Stat(filepath.Join(home, ".supabase/access-token"))
+	require.NoError(suite.T(), err)
+	token, err := ioutil.ReadFile(filepath.Join(home, ".supabase/access-token"))
+	require.NoError(suite.T(), err)
+	require.Equal(suite.T(), key, string(token))
+}
+
+// hooks
+func (suite *LoginTestSuite) SetupTest() {
+	// init cli
+	suite.cmd = clicmd.GetRootCmd()
+	suite.tempDir = NewTempDir(Logger, TempDir)
+
+	// init supabase
+	init, _, err := suite.cmd.Find([]string{"init"})
+	require.NoError(suite.T(), err)
+	require.NoError(suite.T(), init.RunE(init, []string{}))
+
+	// implement mocks
+	SupaMock.FunctionsHandler = func(c *gin.Context) {
+		suite.addHeaders(c.Request.Header)
+		suite.addID(c.Params.ByName("id"))
+
+		c.JSON(http.StatusOK, gin.H{})
+	}
+}
+
+func (suite *LoginTestSuite) TeardownTest() {
+	require.NoError(suite.T(), os.Chdir(TempDir))
+}
+
+// In order for 'go test' to run this suite, we need to create
+// a normal test function and pass our suite to suite.Run
+func TestLoginTestSuite(t *testing.T) {
+	suite.Run(t, new(LoginTestSuite))
+}
+
+// helper functions
+func (suite *LoginTestSuite) addID(id string) {
+	suite.mtx.Lock()
+	defer suite.mtx.Unlock()
+	suite.ids = append(suite.ids, id)
+}
+
+func (suite *LoginTestSuite) addHeaders(headers http.Header) {
+	suite.mtx.Lock()
+	defer suite.mtx.Unlock()
+	suite.headers = append(suite.headers, headers)
+}
diff --git a/test/main_test.go b/test/main_test.go
new file mode 100644
index 00000000..ed93208a
--- /dev/null
+++ b/test/main_test.go
@@ -0,0 +1,99 @@
+package integration
+
+import (
+	"log"
+	"os"
+	"testing"
+
+	"github.com/docker/docker/client"
+	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/test/mocks/docker"
+	"github.com/supabase/cli/test/mocks/supabase"
+)
+
+const (
+	DockerPort   = ":2375"
+	SupabasePort = ":2376"
+)
+
+var (
+	TempDir string
+)
+
+var (
+	Logger     *log.Logger
+	DockerMock *docker.Server
+	SupaMock   *supabase.Server
+)
+
+func TestMain(m *testing.M) {
+	Logger := log.New(os.Stdout, "", 0)
+
+	Logger.Println("Global tests setup")
+
+	DockerMock = newDockerMock(Logger)
+	SupaMock = newSupabaseMock(Logger)
+	TempDir = NewTempDir(Logger, "")
+
+	// redirect clients to mock servers
+	err := client.WithHost("tcp://127.0.0.1" + DockerPort)(utils.Docker)
+	if err != nil {
+		Logger.Fatal(err)
+	}
+	os.Setenv("SUPABASE_INTERNAL_API_HOST", "http://127.0.0.1"+SupabasePort)
+
+	// run tests
+	exitVal := m.Run()
+
+	Logger.Println("Global teardown")
+	os.RemoveAll(TempDir)
+
+	// exit process with tests exit code
+	os.Exit(exitVal)
+}
+
+func newDockerMock(Logger *log.Logger) *docker.Server {
+	dockerMock := docker.NewServer()
+	dockerRouter := dockerMock.NewRouter()
+	go func() {
+		err := dockerRouter.Run(DockerPort)
+		if err != nil {
+			Logger.Fatal(err)
+		}
+	}()
+
+	return dockerMock
+}
+
+func newSupabaseMock(Logger *log.Logger) *supabase.Server {
+	supaMock := supabase.NewServer()
+	supaRouter := supaMock.NewRouter()
+	go func() {
+		err := supaRouter.Run(SupabasePort)
+		if err != nil {
+			Logger.Fatal(err)
+		}
+	}()
+
+	return supaMock
+}
+
+func NewTempDir(Logger *log.Logger, baseDir string) string {
+	wd := baseDir
+	var err error
+	if baseDir == "" {
+		wd, err = os.Getwd()
+	}
+	if err != nil {
+		Logger.Fatal(err)
+	}
+	tempDir, err := os.MkdirTemp(wd, "cli-test-")
+	if err != nil {
+		Logger.Fatal(err)
+	}
+	err = os.Chdir(tempDir)
+	if err != nil {
+		Logger.Fatal(err)
+	}
+	return tempDir
+}
diff --git a/test/migration_test.go b/test/migration_test.go
new file mode 100644
index 00000000..2321735d
--- /dev/null
+++ b/test/migration_test.go
@@ -0,0 +1,56 @@
+package integration
+
+// Basic imports
+import (
+	"os"
+	"testing"
+
+	gonanoid "github.com/matoous/go-nanoid/v2"
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	clicmd "github.com/supabase/cli/cmd"
+	"github.com/supabase/cli/test/mocks/supabase"
+)
+
+type MigrationTestSuite struct {
+	suite.Suite
+	tempDir string
+	cmd     *cobra.Command
+}
+
+// test functions
+func (suite *MigrationTestSuite) TestNewMigration() {
+	// run command
+	migration, _, err := suite.cmd.Find([]string{"migration", "new"})
+	require.NoError(suite.T(), err)
+	name := gonanoid.MustGenerate(supabase.IDAlphabet, 10)
+	require.NoError(suite.T(), migration.RunE(migration, []string{name}))
+
+	// check migrations file created
+	subs, err := os.ReadDir("supabase/migrations")
+	require.NoError(suite.T(), err)
+	require.Regexp(suite.T(), `[0-9]{14}_`+name+".sql", subs[0].Name())
+}
+
+// hooks
+func (suite *MigrationTestSuite) SetupTest() {
+	// init cli
+	suite.cmd = clicmd.GetRootCmd()
+	suite.tempDir = NewTempDir(Logger, TempDir)
+
+	// init supabase
+	init, _, err := suite.cmd.Find([]string{"init"})
+	require.NoError(suite.T(), err)
+	require.NoError(suite.T(), init.RunE(init, []string{}))
+}
+
+func (suite *MigrationTestSuite) TeardownTest() {
+	require.NoError(suite.T(), os.Chdir(TempDir))
+}
+
+// In order for 'go test' to run this suite, we need to create
+// a normal test function and pass our suite to suite.Run
+func TestMigrationTestSuite(t *testing.T) {
+	suite.Run(t, new(MigrationTestSuite))
+}
diff --git a/test/mocks/docker/httputils.go b/test/mocks/docker/httputils.go
new file mode 100644
index 00000000..c8a4496a
--- /dev/null
+++ b/test/mocks/docker/httputils.go
@@ -0,0 +1,67 @@
+package docker
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/docker/docker/pkg/ioutils"
+	"github.com/docker/docker/pkg/stdcopy"
+	"github.com/gin-gonic/gin"
+)
+
+func CloseStreams(streams ...interface{}) {
+	for _, stream := range streams {
+		if tcpc, ok := stream.(interface {
+			CloseWrite() error
+		}); ok {
+			_ = tcpc.CloseWrite()
+		} else if closer, ok := stream.(io.Closer); ok {
+			_ = closer.Close()
+		}
+	}
+}
+
+func HijackedResponse(c *gin.Context, exitCode string, output ...string) {
+	// hijack the connection
+	hijacker, ok := c.Writer.(http.Hijacker)
+	if !ok {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"message": "error hijacking connection",
+		})
+		return
+	}
+	conn, _, err := hijacker.Hijack()
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"message": "error hijacking connection",
+		})
+		return
+	}
+	_, err = conn.Write([]byte{})
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"message": "error hijacking connection",
+		})
+		return
+	}
+
+	// write success code signalizing that the connection is established and ready to stream data
+	fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n")
+
+	// setup closer
+	closer := func() error {
+		CloseStreams(conn)
+		return nil
+	}
+
+	// write some output if command suppose to write to stdout
+	outStream := stdcopy.NewStdWriter(conn, stdcopy.Stdout)
+	if len(output) > 0 {
+		fmt.Fprint(outStream, output)
+	}
+	// finish with exit code and close stream and connection as the command is done
+	fmt.Fprintf(outStream, "exit code %s", exitCode)
+	rc := ioutils.NewReadCloserWrapper(conn, closer)
+	rc.Close()
+}
diff --git a/test/mocks/docker/server.go b/test/mocks/docker/server.go
new file mode 100644
index 00000000..7fffb2ec
--- /dev/null
+++ b/test/mocks/docker/server.go
@@ -0,0 +1,97 @@
+package docker
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+)
+
+const (
+	IDAlphabet = "abcdef0123456789"
+	IDLength   = 12
+)
+
+// Server struct with route handlers
+type Server struct {
+	PingHandler             func(c *gin.Context)
+	ContainerInspectHandler func(c *gin.Context)
+	ExecCreateHandler       func(c *gin.Context)
+	ExecStartHandler        func(c *gin.Context)
+}
+
+var defaultHandler = func(c *gin.Context) {
+	c.JSON(http.StatusNotImplemented, gin.H{
+		"message": "Not implemented",
+	})
+}
+
+// NewServer creates a new server with default handlers
+func NewServer() *Server {
+	s := Server{
+		ExecCreateHandler:       defaultHandler,
+		ExecStartHandler:        defaultHandler,
+		ContainerInspectHandler: defaultHandler,
+	}
+	return &s
+}
+
+// NewRouter creating a new router and setting the routes for the server.
+func (s *Server) NewRouter() *gin.Engine {
+	root := gin.Default()
+	root.HEAD("/_ping", s.ping)
+	root.GET("/_ping", s.ping)
+
+	router := root.Group("/v1.41")
+
+	containers := router.Group("/containers")
+	containers.GET("/:id/json", s.inspectContainer)
+	containers.POST("/:id/exec", s.createExec)
+
+	exec := router.Group("/exec")
+	exec.POST("/:id/start", s.startExec)
+
+	return root
+}
+
+// ping
+func (s *Server) ping(c *gin.Context) {
+	if s.PingHandler == nil {
+		c.Header("API-Version", "1.41")
+		c.Header("OSType", "linux")
+		c.Status(http.StatusOK)
+	} else {
+		s.PingHandler(c)
+	}
+}
+
+// container
+func (s *Server) inspectContainer(c *gin.Context) {
+	if s.ContainerInspectHandler == nil {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": "handler is nil",
+		})
+	} else {
+		s.ContainerInspectHandler(c)
+	}
+}
+
+// exec
+func (s *Server) createExec(c *gin.Context) {
+	if s.ExecCreateHandler == nil {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": "handler is nil",
+		})
+	} else {
+		s.ExecCreateHandler(c)
+	}
+}
+
+func (s *Server) startExec(c *gin.Context) {
+	if s.ExecStartHandler == nil {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": "handler is nil",
+		})
+	} else {
+		s.ExecStartHandler(c)
+	}
+}
diff --git a/test/mocks/supabase/server.go b/test/mocks/supabase/server.go
new file mode 100644
index 00000000..96b0aeeb
--- /dev/null
+++ b/test/mocks/supabase/server.go
@@ -0,0 +1,68 @@
+package supabase
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+)
+
+const (
+	IDAlphabet  = "abcdefghijklmnopqrstuvwxyz"
+	IDLength    = 20
+	KeyAlphabet = "abcdef0123456789"
+	KeyLength   = 40
+)
+
+// Server struct with route handlers
+type Server struct {
+	FunctionsHandler func(c *gin.Context)
+	SecretsHandler   func(c *gin.Context)
+}
+
+var defaultHandler = func(c *gin.Context) {
+	c.JSON(http.StatusNotImplemented, gin.H{
+		"message": "Not implemented",
+	})
+}
+
+// NewServer creates a new server with default handlers
+func NewServer() *Server {
+	s := Server{
+		FunctionsHandler: defaultHandler,
+		SecretsHandler:   defaultHandler,
+	}
+	return &s
+}
+
+// NewRouter creating a new router and setting the routes for the server.
+func (s *Server) NewRouter() *gin.Engine {
+	root := gin.Default()
+	router := root.Group("/v1")
+
+	projects := router.Group("/projects")
+	projects.GET("/:id/functions", s.functions)
+	projects.GET("/:id/secrets", s.secrets)
+
+	return root
+}
+
+// project routes
+func (s *Server) functions(c *gin.Context) {
+	if s.FunctionsHandler == nil {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": "handler is nil",
+		})
+	} else {
+		s.FunctionsHandler(c)
+	}
+}
+
+func (s *Server) secrets(c *gin.Context) {
+	if s.SecretsHandler == nil {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": "handler is nil",
+		})
+	} else {
+		s.SecretsHandler(c)
+	}
+}
diff --git a/test/secrets_test.go b/test/secrets_test.go
new file mode 100644
index 00000000..e87f1415
--- /dev/null
+++ b/test/secrets_test.go
@@ -0,0 +1,122 @@
+package integration
+
+// Basic imports
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"sync"
+	"testing"
+
+	"github.com/gin-gonic/gin"
+	gonanoid "github.com/matoous/go-nanoid/v2"
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	clicmd "github.com/supabase/cli/cmd"
+	"github.com/supabase/cli/test/mocks/supabase"
+)
+
+type SecretsTestSuite struct {
+	suite.Suite
+	tempDir string
+	cmd     *cobra.Command
+
+	ids     []string
+	headers []http.Header
+
+	mtx sync.RWMutex
+}
+
+// test functions
+func (suite *SecretsTestSuite) TestLink() {
+	// run command
+	list, _, err := suite.cmd.Find([]string{"secrets", "list"})
+	require.NoError(suite.T(), err)
+
+	key := "sbp_" + gonanoid.MustGenerate(supabase.KeyAlphabet, supabase.KeyLength)
+	os.Setenv("SUPABASE_ACCESS_TOKEN", key)
+	ref := gonanoid.MustGenerate(supabase.IDAlphabet, supabase.IDLength)
+	require.NoError(suite.T(), ioutil.WriteFile("supabase/.temp/project-ref", []byte(ref), os.FileMode(0755)))
+
+	// set stdout to write into file so we can capture cmd output
+	tmpfile, err := ioutil.TempFile(suite.tempDir, "output")
+	require.NoError(suite.T(), err)
+	defer os.Remove(tmpfile.Name()) // clean up
+	oldStdout := os.Stdout
+	defer func() { os.Stdout = oldStdout }()
+	os.Stdout = tmpfile
+
+	require.NoError(suite.T(), list.RunE(list, []string{}))
+
+	// check request details
+	suite.mtx.RLock()
+	defer suite.mtx.RUnlock()
+	require.Contains(suite.T(), suite.ids, ref)
+	require.Contains(suite.T(), suite.headers, http.Header{
+		"Authorization":   []string{fmt.Sprintf("Bearer %s", key)},
+		"Accept-Encoding": []string{"gzip"},
+		"User-Agent":      []string{"Go-http-client/1.1"},
+	})
+
+	contents, err := ioutil.ReadFile(tmpfile.Name())
+	require.NoError(suite.T(), err)
+	require.Contains(suite.T(), string(contents), "some-key")
+	require.Contains(suite.T(), string(contents), "another")
+}
+
+// hooks
+func (suite *SecretsTestSuite) SetupTest() {
+	// init cli
+	suite.cmd = clicmd.GetRootCmd()
+	suite.tempDir = NewTempDir(Logger, TempDir)
+
+	// init supabase
+	init, _, err := suite.cmd.Find([]string{"init"})
+	require.NoError(suite.T(), err)
+	require.NoError(suite.T(), init.RunE(init, []string{}))
+
+	// add `link` dir
+	require.NoError(suite.T(), os.MkdirAll("supabase/.temp", os.FileMode(0755)))
+
+	// implement mocks
+	SupaMock.SecretsHandler = func(c *gin.Context) {
+		suite.addHeaders(c.Request.Header)
+		suite.addID(c.Params.ByName("id"))
+
+		c.JSON(http.StatusOK, []gin.H{
+			{
+				"Name":  "some-key",
+				"Value": gonanoid.Must(),
+			},
+			{
+				"Name":  "another",
+				"Value": gonanoid.Must(),
+			},
+		})
+	}
+}
+
+func (suite *SecretsTestSuite) TeardownTest() {
+	require.NoError(suite.T(), os.Chdir(TempDir))
+}
+
+// In order for 'go test' to run this suite, we need to create
+// a normal test function and pass our suite to suite.Run
+func TestSecretsTestSuite(t *testing.T) {
+	suite.Run(t, new(SecretsTestSuite))
+}
+
+// helper functions
+func (suite *SecretsTestSuite) addID(id string) {
+	suite.mtx.Lock()
+	defer suite.mtx.Unlock()
+	suite.ids = append(suite.ids, id)
+}
+
+func (suite *SecretsTestSuite) addHeaders(headers http.Header) {
+	suite.mtx.Lock()
+	defer suite.mtx.Unlock()
+	suite.headers = append(suite.headers, headers)
+}
diff --git a/test/status_test.go b/test/status_test.go
new file mode 100644
index 00000000..cf671cbb
--- /dev/null
+++ b/test/status_test.go
@@ -0,0 +1,98 @@
+package integration
+
+// Basic imports
+import (
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+	"sync"
+	"testing"
+
+	"github.com/gin-gonic/gin"
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	clicmd "github.com/supabase/cli/cmd"
+)
+
+type StatusTestSuite struct {
+	suite.Suite
+	tempDir string
+	cmd     *cobra.Command
+
+	params []gin.Params
+	mtx    sync.RWMutex
+}
+
+// test functions
+func (suite *StatusTestSuite) TestStatus() {
+	// run command
+	status, _, err := suite.cmd.Find([]string{"status"})
+	require.NoError(suite.T(), err)
+
+	// set stdout to write into file so we can capture cmd output
+	tmpfile, err := ioutil.TempFile(suite.tempDir, "output")
+	require.NoError(suite.T(), err)
+	defer os.Remove(tmpfile.Name()) // clean up
+	oldStdout := os.Stdout
+	defer func() { os.Stdout = oldStdout }()
+	os.Stdout = tmpfile
+
+	// run command
+	require.NoError(suite.T(), status.RunE(status, []string{}))
+
+	// check request details
+	suite.mtx.RLock()
+	defer suite.mtx.RUnlock()
+	require.Contains(suite.T(), suite.params, gin.Params{
+		gin.Param{
+			Key:   "id",
+			Value: "supabase_db_" + filepath.Base(suite.tempDir),
+		},
+	})
+
+	contents, err := ioutil.ReadFile(tmpfile.Name())
+	require.NoError(suite.T(), err)
+	require.Contains(suite.T(), string(contents), "API URL: http://localhost:54321")
+	require.Contains(suite.T(), string(contents), "DB URL: postgresql://postgres:postgres@localhost:54322/postgres")
+	require.Contains(suite.T(), string(contents), "Studio URL: http://localhost:54323")
+	require.Contains(suite.T(), string(contents), "Inbucket URL: http://localhost:54324")
+	require.Contains(suite.T(), string(contents), "anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")
+	require.Contains(suite.T(), string(contents), "service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")
+}
+
+// hooks
+func (suite *StatusTestSuite) SetupTest() {
+	// init cli
+	suite.cmd = clicmd.GetRootCmd()
+	suite.tempDir = NewTempDir(Logger, TempDir)
+
+	// init supabase
+	init, _, err := suite.cmd.Find([]string{"init"})
+	require.NoError(suite.T(), err)
+	require.NoError(suite.T(), init.RunE(init, []string{}))
+
+	// implement mocks
+	DockerMock.ContainerInspectHandler = func(c *gin.Context) {
+		suite.addParams(c.Copy())
+		c.JSON(http.StatusOK, gin.H{})
+	}
+}
+
+func (suite *StatusTestSuite) TeardownTest() {
+	require.NoError(suite.T(), os.Chdir(TempDir))
+}
+
+// In order for 'go test' to run this suite, we need to create
+// a normal test function and pass our suite to suite.Run
+func TestStatusTestSuite(t *testing.T) {
+	suite.Run(t, new(StatusTestSuite))
+}
+
+// helper functions
+func (suite *StatusTestSuite) addParams(c *gin.Context) {
+	suite.mtx.Lock()
+	defer suite.mtx.Unlock()
+	suite.params = append(suite.params, c.Params)
+}