supabase-cli/internal/db/lint/lint_test.go

300 lines
8.9 KiB
Go

package lint
import (
"bytes"
"context"
"encoding/json"
"net/http"
"testing"
"github.com/docker/docker/api/types"
"github.com/h2non/gock"
"github.com/jackc/pgconn"
"github.com/jackc/pgerrcode"
"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/pkg/pgtest"
)
var dbConfig = pgconn.Config{
Host: "127.0.0.1",
Port: 5432,
User: "admin",
Password: "password",
Database: "postgres",
}
func TestLintCommand(t *testing.T) {
utils.Config.Hostname = "127.0.0.1"
utils.Config.Db.Port = 5432
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock docker
require.NoError(t, apitest.MockDocker(utils.Docker))
defer gock.OffAll()
gock.New(utils.Docker.DaemonHost()).
Get("/v" + utils.Docker.ClientVersion() + "/containers").
Reply(http.StatusOK).
JSON(types.ContainerJSON{})
// Setup db response
expected := Result{
Function: "22751",
Issues: []Issue{{
Level: AllowedLevels[1],
Message: `record "r" has no field "c"`,
Statement: &Statement{
LineNumber: "6",
Text: "RAISE",
},
Context: `SQL expression "r.c"`,
SQLState: pgerrcode.UndefinedColumn,
}},
}
data, err := json.Marshal(expected)
require.NoError(t, err)
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query("begin").Reply("BEGIN").
Query(ENABLE_PGSQL_CHECK).
Reply("CREATE EXTENSION").
Query(checkSchemaScript, "public").
Reply("SELECT 1", []interface{}{"f1", string(data)}).
Query("rollback").Reply("ROLLBACK")
// Run test
err = Run(context.Background(), []string{"public"}, "warning", "none", dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
assert.Empty(t, apitest.ListUnmatchedRequests())
}
func TestLintDatabase(t *testing.T) {
t.Run("parses lint results", func(t *testing.T) {
expected := []Result{{
Function: "public.f1",
Issues: []Issue{{
Level: AllowedLevels[1],
Message: `record "r" has no field "c"`,
Statement: &Statement{
LineNumber: "6",
Text: "RAISE",
},
Context: `SQL expression "r.c"`,
SQLState: pgerrcode.UndefinedColumn,
}, {
Level: "warning extra",
Message: `never read variable "entity"`,
SQLState: pgerrcode.SuccessfulCompletion,
}},
}, {
Function: "public.f2",
Issues: []Issue{{
Level: AllowedLevels[1],
Message: `relation "t2" does not exist`,
Statement: &Statement{
LineNumber: "4",
Text: "FOR over SELECT rows",
},
Query: &Query{
Position: "15",
Text: "SELECT * FROM t2",
},
SQLState: pgerrcode.UndefinedTable,
}},
}}
r1, err := json.Marshal(expected[0])
require.NoError(t, err)
r2, err := json.Marshal(expected[1])
require.NoError(t, err)
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query("begin").Reply("BEGIN").
Query(ENABLE_PGSQL_CHECK).
Reply("CREATE EXTENSION").
Query(checkSchemaScript, "public").
Reply("SELECT 2",
[]interface{}{"f1", string(r1)},
[]interface{}{"f2", string(r2)},
).
Query("rollback").Reply("ROLLBACK")
// Run test
result, err := LintDatabase(context.Background(), conn.MockClient(t), []string{"public"})
assert.NoError(t, err)
// Validate result
assert.ElementsMatch(t, expected, result)
})
t.Run("supports multiple schema", func(t *testing.T) {
expected := []Result{{
Function: "public.where_clause",
Issues: []Issue{{
Level: AllowedLevels[0],
Message: "target type is different type than source type",
Statement: &Statement{
LineNumber: "32",
Text: "statement block",
},
Hint: "The input expression type does not have an assignment cast to the target type.",
Detail: `cast "text" value to "text[]" type`,
Context: `during statement block local variable "clause_arr" initialization on line 3`,
SQLState: pgerrcode.DatatypeMismatch,
}},
}, {
Function: "private.f2",
Issues: []Issue{},
}}
r1, err := json.Marshal(expected[0])
require.NoError(t, err)
r2, err := json.Marshal(expected[1])
require.NoError(t, err)
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query("begin").Reply("BEGIN").
Query(ENABLE_PGSQL_CHECK).
Reply("CREATE EXTENSION").
Query(checkSchemaScript, "public").
Reply("SELECT 1", []interface{}{"where_clause", string(r1)}).
Query(checkSchemaScript, "private").
Reply("SELECT 1", []interface{}{"f2", string(r2)}).
Query("rollback").Reply("ROLLBACK")
// Run test
result, err := LintDatabase(context.Background(), conn.MockClient(t), []string{"public", "private"})
// Check error
assert.NoError(t, err)
assert.ElementsMatch(t, expected, result)
})
t.Run("throws error on missing extension", func(t *testing.T) {
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query("begin").Reply("BEGIN").
Query(ENABLE_PGSQL_CHECK).
ReplyError(pgerrcode.UndefinedFile, `could not open extension control file "/usr/share/postgresql/14/extension/plpgsql_check.control": No such file or directory"`).
Query("rollback").Reply("ROLLBACK")
// Run test
_, err := LintDatabase(context.Background(), conn.MockClient(t), []string{"public"})
// Check error
assert.Error(t, err)
})
t.Run("throws error on malformed json", func(t *testing.T) {
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query("begin").Reply("BEGIN").
Query(ENABLE_PGSQL_CHECK).
Reply("CREATE EXTENSION").
Query(checkSchemaScript, "public").
Reply("SELECT 1", []interface{}{"f1", "malformed"}).
Query("rollback").Reply("ROLLBACK")
// Run test
_, err := LintDatabase(context.Background(), conn.MockClient(t), []string{"public"})
// Check error
assert.Error(t, err)
})
}
func TestPrintResult(t *testing.T) {
result := []Result{{
Function: "public.f1",
Issues: []Issue{{
Level: "warning",
Message: "test 1a",
}, {
Level: "error",
Message: "test 1b",
}},
}, {
Function: "private.f2",
Issues: []Issue{{
Level: "warning extra",
Message: "test 2",
}},
}}
t.Run("filters warning level", func(t *testing.T) {
// Run test
var out bytes.Buffer
filtered := filterResult(result, toEnum("warning"))
assert.NoError(t, printResultJSON(filtered, &out))
// Validate output
var actual []Result
assert.NoError(t, json.Unmarshal(out.Bytes(), &actual))
assert.ElementsMatch(t, result, actual)
})
t.Run("filters error level", func(t *testing.T) {
// Run test
var out bytes.Buffer
filtered := filterResult(result, toEnum("error"))
assert.NoError(t, printResultJSON(filtered, &out))
// Validate output
var actual []Result
assert.NoError(t, json.Unmarshal(out.Bytes(), &actual))
assert.ElementsMatch(t, []Result{{
Function: result[0].Function,
Issues: []Issue{result[0].Issues[1]},
}}, actual)
})
t.Run("exits with non-zero status on warning", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query("begin").Reply("BEGIN").
Query(ENABLE_PGSQL_CHECK).
Reply("CREATE EXTENSION").
Query(checkSchemaScript, "public").
Reply("SELECT 1", []interface{}{"f1", `{"function":"22751","issues":[{"level":"warning","message":"test warning"}]}`}).
Query("rollback").Reply("ROLLBACK")
// Run test
err := Run(context.Background(), []string{"public"}, "warning", "warning", dbConfig, fsys, conn.Intercept)
// Check error
assert.ErrorContains(t, err, "fail-on is set to warning, non-zero exit")
})
t.Run("exits with non-zero status on error", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query("begin").Reply("BEGIN").
Query(ENABLE_PGSQL_CHECK).
Reply("CREATE EXTENSION").
Query(checkSchemaScript, "public").
Reply("SELECT 1", []interface{}{"f1", `{"function":"22751","issues":[{"level":"error","message":"test error"}]}`}).
Query("rollback").Reply("ROLLBACK")
// Run test
err := Run(context.Background(), []string{"public"}, "warning", "error", dbConfig, fsys, conn.Intercept)
// Check error
assert.ErrorContains(t, err, "fail-on is set to error, non-zero exit")
})
t.Run("does not exit with non-zero status when fail-on is none", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query("begin").Reply("BEGIN").
Query(ENABLE_PGSQL_CHECK).
Reply("CREATE EXTENSION").
Query(checkSchemaScript, "public").
Reply("SELECT 1", []interface{}{"f1", `{"function":"22751","issues":[{"level":"error","message":"test error"}]}`}).
Query("rollback").Reply("ROLLBACK")
// Run test
err := Run(context.Background(), []string{"public"}, "warning", "none", dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
})
}