300 lines
8.9 KiB
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)
|
|
})
|
|
}
|