supabase-cli/internal/functions/deploy/deploy_test.go

351 lines
13 KiB
Go

package deploy
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"testing"
"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"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/api"
"github.com/supabase/cli/pkg/cast"
"github.com/supabase/cli/pkg/config"
)
func TestDeployCommand(t *testing.T) {
flags.ProjectRef = apitest.RandomProjectRef()
const slug = "test-func"
const containerId = "test-container"
imageUrl := utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image)
t.Run("deploys multiple functions", func(t *testing.T) {
functions := []string{slug, slug + "-2"}
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, utils.WriteConfig(fsys, false))
// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
// Setup valid deno path
_, err := fsys.Create(utils.DenoPathOverride)
require.NoError(t, err)
// Setup mock api
defer gock.OffAll()
gock.New(utils.DefaultApiHost).
Get("/v1/projects/" + flags.ProjectRef + "/functions").
Reply(http.StatusOK).
JSON([]api.FunctionResponse{})
for i := range functions {
// Do not match slug to avoid flakey tests
gock.New(utils.DefaultApiHost).
Post("/v1/projects/" + flags.ProjectRef + "/functions").
Reply(http.StatusCreated).
JSON(api.FunctionResponse{Id: fmt.Sprintf("%d", i)})
// Setup mock docker
require.NoError(t, apitest.MockDocker(utils.Docker))
apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
}
// Setup output file
for _, v := range functions {
outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", v))
require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
}
// Run test
noVerifyJWT := true
err = Run(context.Background(), functions, true, &noVerifyJWT, "", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("deploys functions from config", func(t *testing.T) {
t.Cleanup(func() { clear(utils.Config.Functions) })
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, utils.WriteConfig(fsys, false))
f, err := fsys.OpenFile(utils.ConfigPath, os.O_APPEND|os.O_WRONLY, 0600)
require.NoError(t, err)
_, err = f.WriteString(`
[functions.` + slug + `]
import_map = "./import_map.json"
`)
require.NoError(t, err)
require.NoError(t, f.Close())
importMapPath, err := filepath.Abs(filepath.Join(utils.SupabaseDirPath, "import_map.json"))
require.NoError(t, err)
require.NoError(t, afero.WriteFile(fsys, importMapPath, []byte("{}"), 0644))
// Setup function entrypoint
entrypointPath := filepath.Join(utils.FunctionsDir, slug, "index.ts")
require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644))
ignorePath := filepath.Join(utils.FunctionsDir, "_ignore", "index.ts")
require.NoError(t, afero.WriteFile(fsys, ignorePath, []byte{}, 0644))
// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
// Setup valid deno path
_, err = fsys.Create(utils.DenoPathOverride)
require.NoError(t, err)
// Setup mock api
defer gock.OffAll()
gock.New(utils.DefaultApiHost).
Get("/v1/projects/" + flags.ProjectRef + "/functions").
Reply(http.StatusOK).
JSON([]api.FunctionResponse{})
gock.New(utils.DefaultApiHost).
Post("/v1/projects/"+flags.ProjectRef+"/functions").
MatchParam("slug", slug).
ParamPresent("import_map_path").
Reply(http.StatusCreated).
JSON(api.FunctionResponse{Id: "1"})
// Setup mock docker
require.NoError(t, apitest.MockDocker(utils.Docker))
apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
// Setup output file
outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug))
require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
// Run test
err = Run(context.Background(), nil, true, nil, "", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("skip disabled functions from config", func(t *testing.T) {
t.Cleanup(func() { clear(utils.Config.Functions) })
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, utils.WriteConfig(fsys, false))
f, err := fsys.OpenFile(utils.ConfigPath, os.O_APPEND|os.O_WRONLY, 0600)
require.NoError(t, err)
_, err = f.WriteString(`
[functions.disabled-func]
enabled = false
import_map = "./import_map.json"
`)
require.NoError(t, err)
require.NoError(t, f.Close())
importMapPath, err := filepath.Abs(filepath.Join(utils.SupabaseDirPath, "import_map.json"))
require.NoError(t, err)
require.NoError(t, afero.WriteFile(fsys, importMapPath, []byte("{}"), 0644))
// Setup function entrypoints
require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "enabled-func", "index.ts"), []byte{}, 0644))
require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "disabled-func", "index.ts"), []byte{}, 0644))
// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
// Setup valid deno path
_, err = fsys.Create(utils.DenoPathOverride)
require.NoError(t, err)
// Setup mock api
defer gock.OffAll()
gock.New(utils.DefaultApiHost).
Get("/v1/projects/" + flags.ProjectRef + "/functions").
Reply(http.StatusOK).
JSON([]api.FunctionResponse{})
gock.New(utils.DefaultApiHost).
Post("/v1/projects/"+flags.ProjectRef+"/functions").
MatchParam("slug", "enabled-func").
Reply(http.StatusCreated).
JSON(api.FunctionResponse{Id: "1"})
require.NoError(t, apitest.MockDocker(utils.Docker))
apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
// Setup output file
outputDir := filepath.Join(utils.TempDir, ".output_enabled-func")
require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
// Run test
err = Run(context.Background(), nil, true, nil, "", 1, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("throws error on malformed slug", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, utils.WriteConfig(fsys, false))
// Run test
err := Run(context.Background(), []string{"_invalid"}, true, nil, "", 1, fsys)
// Check error
assert.ErrorContains(t, err, "Invalid Function name.")
})
t.Run("throws error on empty functions", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, utils.WriteConfig(fsys, false))
// Run test
err := Run(context.Background(), nil, true, nil, "", 1, fsys)
// Check error
assert.ErrorContains(t, err, "No Functions specified or found in supabase/functions")
})
t.Run("verify_jwt param falls back to config", func(t *testing.T) {
t.Cleanup(func() { clear(utils.Config.Functions) })
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, utils.WriteConfig(fsys, false))
f, err := fsys.OpenFile(utils.ConfigPath, os.O_APPEND|os.O_WRONLY, 0600)
require.NoError(t, err)
_, err = f.WriteString(`
[functions.` + slug + `]
verify_jwt = false
`)
require.NoError(t, err)
require.NoError(t, f.Close())
// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
// Setup valid deno path
_, err = fsys.Create(utils.DenoPathOverride)
require.NoError(t, err)
// Setup mock api
defer gock.OffAll()
gock.New(utils.DefaultApiHost).
Get("/v1/projects/" + flags.ProjectRef + "/functions").
Reply(http.StatusOK).
JSON([]api.FunctionResponse{})
gock.New(utils.DefaultApiHost).
Post("/v1/projects/"+flags.ProjectRef+"/functions").
MatchParam("verify_jwt", "false").
Reply(http.StatusCreated).
JSON(api.FunctionResponse{Id: "1"})
// Setup mock docker
require.NoError(t, apitest.MockDocker(utils.Docker))
apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
// Setup output file
outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug))
require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
// Run test
assert.NoError(t, Run(context.Background(), []string{slug}, true, nil, "", 1, fsys))
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
t.Run("verify_jwt flag overrides config", func(t *testing.T) {
t.Cleanup(func() { clear(utils.Config.Functions) })
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, utils.WriteConfig(fsys, false))
f, err := fsys.OpenFile(utils.ConfigPath, os.O_APPEND|os.O_WRONLY, 0600)
require.NoError(t, err)
_, err = f.WriteString(`
[functions.` + slug + `]
verify_jwt = false
`)
require.NoError(t, err)
require.NoError(t, f.Close())
// Setup valid access token
token := apitest.RandomAccessToken(t)
t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
// Setup valid deno path
_, err = fsys.Create(utils.DenoPathOverride)
require.NoError(t, err)
// Setup mock api
defer gock.OffAll()
gock.New(utils.DefaultApiHost).
Get("/v1/projects/" + flags.ProjectRef + "/functions").
Reply(http.StatusOK).
JSON([]api.FunctionResponse{})
gock.New(utils.DefaultApiHost).
Post("/v1/projects/"+flags.ProjectRef+"/functions").
MatchParam("verify_jwt", "true").
Reply(http.StatusCreated).
JSON(api.FunctionResponse{Id: "1"})
// Setup mock docker
require.NoError(t, apitest.MockDocker(utils.Docker))
apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled"))
// Setup output file
outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug))
require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644))
// Run test
noVerifyJwt := false
assert.NoError(t, Run(context.Background(), []string{slug}, true, &noVerifyJwt, "", 1, fsys))
// Validate api
assert.Empty(t, apitest.ListUnmatchedRequests())
})
}
func TestImportMapPath(t *testing.T) {
t.Run("loads import map from default location", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644))
// Run test
fc, err := GetFunctionConfig([]string{"test"}, "", nil, fsys)
// Check error
assert.NoError(t, err)
assert.Equal(t, utils.FallbackImportMapPath, fc["test"].ImportMap)
})
t.Run("per function config takes precedence", func(t *testing.T) {
t.Cleanup(func() { clear(utils.Config.Functions) })
slug := "hello"
utils.Config.Functions = config.FunctionConfig{
slug: {ImportMap: "import_map.json"},
}
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644))
// Run test
fc, err := GetFunctionConfig([]string{slug}, "", nil, fsys)
// Check error
assert.NoError(t, err)
assert.Equal(t, "import_map.json", fc[slug].ImportMap)
})
t.Run("overrides with cli flag", func(t *testing.T) {
t.Cleanup(func() { clear(utils.Config.Functions) })
slug := "hello"
utils.Config.Functions = config.FunctionConfig{
slug: {ImportMap: "import_map.json"},
}
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Custom global import map loaded via cli flag
customImportMapPath := filepath.Join(utils.FunctionsDir, "custom_import_map.json")
require.NoError(t, afero.WriteFile(fsys, customImportMapPath, []byte("{}"), 0644))
// Create fallback import map to test precedence order
require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644))
// Run test
fc, err := GetFunctionConfig([]string{slug}, customImportMapPath, cast.Ptr(false), fsys)
// Check error
assert.NoError(t, err)
assert.Equal(t, customImportMapPath, fc[slug].ImportMap)
})
t.Run("returns empty string if no fallback", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Run test
fc, err := GetFunctionConfig([]string{"test"}, "", nil, fsys)
// Check error
assert.NoError(t, err)
assert.Empty(t, fc["test"].ImportMap)
})
t.Run("preserves absolute path", func(t *testing.T) {
path := "/tmp/import_map.json"
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644))
// Run test
fc, err := GetFunctionConfig([]string{"test"}, path, nil, fsys)
// Check error
assert.NoError(t, err)
assert.Equal(t, path, fc["test"].ImportMap)
})
}