Files
2026-09-07 19:28:31 +01:00

231 lines
7.3 KiB
Go

package sqlguard
import (
"strings"
"testing"
)
func TestAnalyzeSourceFindsEveryRiskClass(t *testing.T) {
source := `package fixture
func unsafe(db interface{ Exec(string, ...any) }) {
db.Exec("SELECT * FROM items WHERE id = ?", "id")
db.Exec("INSERT OR IGNORE INTO items (id) VALUES (?)", "id")
}
var schema = "CREATE TABLE items (enabled BOOLEAN DEFAULT 1, created_at DATETIME)"
var sqlite = "SELECT datetime('now') FROM sqlite_master WHERE name = 'items'"
`
findings, err := AnalyzeSource("testdata/unsafe.go", []byte(source), nil)
if err != nil {
t.Fatalf("AnalyzeSource() error = %v", err)
}
wantRules := []Rule{
RuleRawPlaceholder,
RuleConflictSyntax,
RuleBooleanInteger,
RuleDateTimeType,
RuleSQLiteDateFunction,
RuleSQLiteCatalog,
}
for _, rule := range wantRules {
if !hasRule(findings, rule) {
t.Errorf("AnalyzeSource() did not report %q; findings = %#v", rule, findings)
}
}
}
func TestAnalyzeSourceAcceptsPortableQueries(t *testing.T) {
source := `package fixture
func safe(db interface {
Exec(string, ...any)
Rebind(string) string
}) {
db.Exec(db.Rebind("SELECT * FROM items WHERE id = ?"), "id")
}
var schema = "CREATE TABLE items (enabled INTEGER DEFAULT 0, created_at {{timestamp}})"
`
findings, err := AnalyzeSource("testdata/safe.go", []byte(source), nil)
if err != nil {
t.Fatalf("AnalyzeSource() error = %v", err)
}
if len(findings) != 0 {
t.Fatalf("AnalyzeSource() findings = %#v, want none", findings)
}
}
func TestAnalyzeSourceAppliesExactExemptions(t *testing.T) {
source := `package fixture
var sqlite = "SELECT name FROM sqlite_master WHERE type = 'table'"
`
exemptions := []Exemption{{
File: "internal/db/schema.go",
Symbol: "sqlite",
Rule: RuleSQLiteCatalog,
Reason: "the central schema probe is the dialect boundary",
}}
findings, err := AnalyzeSource("internal/db/schema.go", []byte(source), exemptions)
if err != nil {
t.Fatalf("AnalyzeSource() error = %v", err)
}
if len(findings) != 0 {
t.Fatalf("AnalyzeSource() findings = %#v, want exemption", findings)
}
findings, err = AnalyzeSource("internal/db/other.go", []byte(source), exemptions)
if err != nil {
t.Fatalf("AnalyzeSource(other) error = %v", err)
}
if len(findings) == 0 {
t.Fatal("AnalyzeSource(other) findings = none, want exact-file exemption mismatch")
}
}
func TestValidateExemptionsRejectsBroadAndDuplicateEntries(t *testing.T) {
for _, exemptions := range [][]Exemption{
{{File: "internal/*.go", Symbol: "x", Rule: RuleSQLiteCatalog, Reason: "too broad"}},
{{File: "internal/x.go", Symbol: "x", Rule: "unknown", Reason: "unknown rule"}},
{
{File: "internal/x.go", Symbol: "x", Rule: RuleSQLiteCatalog, Reason: "one"},
{File: "internal/x.go", Symbol: "x", Rule: RuleSQLiteCatalog, Reason: "two"},
},
} {
if err := ValidateExemptions(exemptions); err == nil {
t.Fatalf("ValidateExemptions(%#v) error = nil", exemptions)
}
}
}
func hasRule(findings []Finding, want Rule) bool {
for _, finding := range findings {
if finding.Rule == want {
return true
}
}
return false
}
func TestAnalyzeSourceReportsUsefulLocations(t *testing.T) {
findings, err := AnalyzeSource("fixture.go", []byte("package p\nfunc f(db interface{ Exec(string, ...any) }) { db.Exec(\"SELECT ?\") }\n"), nil)
if err != nil {
t.Fatalf("AnalyzeSource() error = %v", err)
}
if len(findings) == 0 || findings[0].Line == 0 || !strings.Contains(findings[0].Message, "placeholder") {
t.Fatalf("finding = %#v, want line and placeholder message", findings)
}
}
func TestAnalyzeSourceFindsStandalonePragma(t *testing.T) {
source := "package p\nfunc unsafe(db interface{ Exec(string, ...any) }) { db.Exec(\"PRAGMA table_info(items)\") }\n"
findings, err := AnalyzeSource("fixture.go", []byte(source), nil)
if err != nil {
t.Fatalf("AnalyzeSource() error = %v", err)
}
if !hasRule(findings, RuleSQLiteCatalog) {
t.Fatalf("AnalyzeSource() findings = %#v, want sqlite-catalog finding", findings)
}
}
func TestAnalyzeSourceFindsAssignedRawPlaceholder(t *testing.T) {
source := `package fixture
func unsafe(db interface{ Exec(string, ...any) }) {
query := "SELECT value FROM settings WHERE key = ?"
db.Exec(query)
}
`
findings, err := AnalyzeSource("fixture.go", []byte(source), nil)
if err != nil {
t.Fatalf("AnalyzeSource() error = %v", err)
}
if !hasRule(findings, RuleRawPlaceholder) {
t.Fatalf("AnalyzeSource() findings = %#v, want assigned raw-placeholder finding", findings)
}
}
func TestAnalyzeSourceRequiresRebindAfterSQLxIn(t *testing.T) {
unsafeSource := `package fixture
func unsafe(db interface{ Exec(string, ...any) }) {
db.Exec(sqlx.In("SELECT value FROM settings WHERE key IN (?)", []string{"x"}))
}
`
findings, err := AnalyzeSource("fixture.go", []byte(unsafeSource), nil)
if err != nil {
t.Fatalf("AnalyzeSource(unsafe) error = %v", err)
}
if !hasRule(findings, RuleRawPlaceholder) {
t.Fatalf("AnalyzeSource(unsafe) findings = %#v, want raw-placeholder finding", findings)
}
safeSource := `package fixture
func safe(db interface{ Exec(string, ...any) }) {
db.Exec(db.Rebind(sqlx.In("SELECT value FROM settings WHERE key IN (?)", []string{"x"})))
}
`
findings, err = AnalyzeSource("fixture.go", []byte(safeSource), nil)
if err != nil {
t.Fatalf("AnalyzeSource(safe) error = %v", err)
}
if hasRule(findings, RuleRawPlaceholder) {
t.Fatalf("AnalyzeSource(safe) findings = %#v, want no raw-placeholder finding", findings)
}
assignedSafeSource := `package fixture
func safeAssigned(db interface{ Exec(string, ...any) }) {
query, args, err := sqlx.In("SELECT value FROM settings WHERE key IN (?)", []string{"x"})
if err != nil { return }
db.Exec(db.Rebind(query), args...)
}
`
findings, err = AnalyzeSource("fixture.go", []byte(assignedSafeSource), nil)
if err != nil {
t.Fatalf("AnalyzeSource(assigned safe) error = %v", err)
}
if hasRule(findings, RuleRawPlaceholder) {
t.Fatalf("AnalyzeSource(assigned safe) findings = %#v, want no raw-placeholder finding", findings)
}
}
func TestAnalyzeSourceFindsBooleanIntegerComparison(t *testing.T) {
source := `package fixture
var query = "UPDATE settings SET enabled = 1 WHERE key = 'x'"
`
findings, err := AnalyzeSource("fixture.go", []byte(source), nil)
if err != nil {
t.Fatalf("AnalyzeSource() error = %v", err)
}
if !hasRule(findings, RuleBooleanInteger) {
t.Fatalf("AnalyzeSource() findings = %#v, want boolean-integer comparison finding", findings)
}
}
func TestAnalyzeSourceIgnoresPortableAndNonBooleanComparisons(t *testing.T) {
source := `package fixture
var portable = "UPDATE settings SET enabled = TRUE WHERE status = 1"
var numeric = "UPDATE settings SET retry_count = 1 WHERE status = 0"
`
findings, err := AnalyzeSource("fixture.go", []byte(source), nil)
if err != nil {
t.Fatalf("AnalyzeSource() error = %v", err)
}
if hasRule(findings, RuleBooleanInteger) {
t.Fatalf("AnalyzeSource() findings = %#v, want no boolean-integer finding", findings)
}
}
func TestAnalyzeSourceFindsKnownBooleanColumnComparison(t *testing.T) {
source := `package fixture
var query = "SELECT id FROM settings WHERE is_enabled != 0"
`
findings, err := AnalyzeSource("fixture.go", []byte(source), nil)
if err != nil {
t.Fatalf("AnalyzeSource() error = %v", err)
}
if !hasRule(findings, RuleBooleanInteger) {
t.Fatalf("AnalyzeSource() findings = %#v, want boolean-integer comparison finding", findings)
}
}