Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

461 lines
12 KiB
Go

package astquery
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// runDetector is a small test helper that invokes RunOnSource with
// the named detector and returns the result. Tests assert on
// .Total / .Matches[*].Line so a query-level regression (a
// detector that compiles but matches the wrong shape) fails loud.
func runDetector(t *testing.T, name, lang, file, src string) Result {
t.Helper()
res, err := RunOnSource(context.Background(), Options{
Detector: name,
// Test fixtures use abstract paths like "lib.go" that
// IsTestPath flags as non-test, so detectors with
// ExcludeTests=true still fire.
}, file, lang, []byte(src))
require.NoError(t, err)
return res
}
func TestDetector_ErrorNotWrapped_FiresOnPassthrough(t *testing.T) {
src := `package x
func F() error {
if err := do(); err != nil {
return err
}
return nil
}
`
res := runDetector(t, "error-not-wrapped", "go", "lib.go", src)
require.Equal(t, 1, res.Total, "expected one match for plain pass-through")
assert.Equal(t, "error-not-wrapped", res.Matches[0].Detector)
assert.Equal(t, "warning", res.Matches[0].Severity)
}
func TestDetector_ErrorNotWrapped_SkipsWrappedReturns(t *testing.T) {
src := `package x
import "fmt"
func F() error {
if err := do(); err != nil {
return fmt.Errorf("doing: %w", err)
}
return nil
}
`
res := runDetector(t, "error-not-wrapped", "go", "lib.go", src)
assert.Equal(t, 0, res.Total, "wrapped errors must not match")
}
func TestDetector_SQLStringConcat_Go(t *testing.T) {
src := `package x
func F(db *sql.DB, name string) {
rows, _ := db.Query("SELECT * FROM users WHERE name = '" + name + "'")
_ = rows
}
`
res := runDetector(t, "sql-string-concat", "go", "lib.go", src)
require.GreaterOrEqual(t, res.Total, 1)
assert.Equal(t, "error", res.Matches[0].Severity)
}
func TestDetector_SQLStringConcat_GoParameterised(t *testing.T) {
src := `package x
func F(db *sql.DB, name string) {
rows, _ := db.Query("SELECT * FROM users WHERE name = ?", name)
_ = rows
}
`
res := runDetector(t, "sql-string-concat", "go", "lib.go", src)
assert.Equal(t, 0, res.Total, "parameterised query must not match")
}
func TestDetector_WeakCrypto_Go(t *testing.T) {
src := `package x
import (
"crypto/md5"
"crypto/sha256"
)
func F() {
_ = md5.New()
_ = sha256.New()
}
`
res := runDetector(t, "weak-crypto", "go", "lib.go", src)
require.Equal(t, 1, res.Total, "only md5.New() should match")
}
func TestDetector_WeakCrypto_Python(t *testing.T) {
src := `import hashlib
def f():
h1 = hashlib.md5(b"x")
h2 = hashlib.sha256(b"x")
return h1, h2
`
res := runDetector(t, "weak-crypto", "python", "lib.py", src)
require.Equal(t, 1, res.Total, "only hashlib.md5(...) should match")
}
func TestDetector_PanicInLibrary_Go(t *testing.T) {
src := `package x
func F() {
panic("nope")
}
`
res := runDetector(t, "panic-in-library", "go", "lib.go", src)
assert.Equal(t, 1, res.Total)
}
func TestDetector_PanicInLibrary_SkipsTestFiles(t *testing.T) {
// Path ending in _test.go should be classified as a test
// and skipped by the test-exclusion gate.
src := `package x
func F() {
panic("ok in tests")
}
`
res := runDetector(t, "panic-in-library", "go", "lib_test.go", src)
assert.Equal(t, 0, res.Total, "panic in test file must not flag")
}
func TestDetector_GoroutineWithoutRecover_Fires(t *testing.T) {
src := `package x
func F() {
go func() {
doSomething()
}()
}
`
res := runDetector(t, "goroutine-without-recover", "go", "lib.go", src)
assert.Equal(t, 1, res.Total)
}
func TestDetector_GoroutineWithoutRecover_SkipsRecoveredBody(t *testing.T) {
src := `package x
func F() {
go func() {
defer func() { _ = recover() }()
doSomething()
}()
}
`
res := runDetector(t, "goroutine-without-recover", "go", "lib.go", src)
assert.Equal(t, 0, res.Total)
}
func TestDetector_HTTPClientNoTimeout_Fires(t *testing.T) {
src := `package x
import "net/http"
func F() *http.Client {
return &http.Client{}
}
`
res := runDetector(t, "http-client-no-timeout", "go", "lib.go", src)
assert.Equal(t, 1, res.Total)
}
func TestDetector_HTTPClientNoTimeout_SkipsExplicitTimeout(t *testing.T) {
src := `package x
import (
"net/http"
"time"
)
func F() *http.Client {
return &http.Client{Timeout: 5 * time.Second}
}
`
res := runDetector(t, "http-client-no-timeout", "go", "lib.go", src)
assert.Equal(t, 0, res.Total)
}
func TestDetector_HardcodedSecret_Go(t *testing.T) {
// Camel-case `apiKey` is the canonical Go name for the
// credential — the regex must be case-insensitive to catch it.
// Values avoid markers ("example", "todo", "your-", …) so the
// placeholder-rejection post-filter doesn't drop them.
src := `package x
func F() {
password := "hunter2hunter2hunter"
apiKey := "AKIA0123456789ABCDEF"
emptyDefault := ""
_ = password
_ = apiKey
_ = emptyDefault
}
`
res := runDetector(t, "hardcoded-secret", "go", "lib.go", src)
require.Equal(t, 2, res.Total, "expect both password (snake) and apiKey (camel)")
}
func TestDetector_HardcodedSecret_Python(t *testing.T) {
src := `password = "hunter2hunter2hunter"
api_key = "AKIA0123456789ABCDEF"
empty_default = ""
placeholder = "TODO_set_me"
`
res := runDetector(t, "hardcoded-secret", "python", "lib.py", src)
require.Equal(t, 2, res.Total)
}
func TestDetector_EmptyCatch_JavaScript(t *testing.T) {
src := `function f() {
try {
risky();
} catch (e) {
}
try {
risky();
} catch (e) {
log(e);
}
}
`
res := runDetector(t, "empty-catch", "javascript", "lib.js", src)
assert.Equal(t, 1, res.Total)
}
func TestDetector_EmptyCatch_Python(t *testing.T) {
src := `def f():
try:
risky()
except Exception:
pass
try:
risky()
except Exception as e:
log(e)
`
res := runDetector(t, "empty-catch", "python", "lib.py", src)
assert.Equal(t, 1, res.Total)
}
func TestDetector_JavaStringEquality_Fires(t *testing.T) {
src := `class C {
boolean f(String s) {
return s == "foo";
}
boolean g(String s) {
return "bar" == s;
}
boolean h(String s) {
return s.equals("safe");
}
}
`
res := runDetector(t, "java-string-equality", "java", "C.java", src)
require.Equal(t, 2, res.Total, "two `==` comparisons must match; .equals() must not")
}
func TestDetector_PythonMutableDefault_Fires(t *testing.T) {
src := `def f(items=[]):
items.append(1)
return items
def g(opts={}):
return opts
def h(x=None):
return x
`
res := runDetector(t, "python-mutable-default-arg", "python", "lib.py", src)
require.Equal(t, 2, res.Total, "list and dict defaults match; None must not")
}
func TestListDetectors_TenBundled(t *testing.T) {
names := ListDetectors()
require.GreaterOrEqual(t, len(names), 10, "expected at least 10 bundled detectors")
want := map[string]bool{
"error-not-wrapped": false,
"sql-string-concat": false,
"weak-crypto": false,
"panic-in-library": false,
"goroutine-without-recover": false,
"http-client-no-timeout": false,
"hardcoded-secret": false,
"empty-catch": false,
"java-string-equality": false,
"python-mutable-default-arg": false,
"unsafe-rust-unwrap": false,
"unsafe-rust-panic-macro": false,
"unsafe-rust-assert-macro": false,
"unsafe-rust-block": false,
"unsafe-python-assert": false,
"unsafe-js-throw": false,
}
for _, n := range names {
if _, ok := want[n]; ok {
want[n] = true
}
}
for n, present := range want {
assert.True(t, present, "detector %q should be registered", n)
}
}
func TestDetector_UnsafeRustUnwrap_Fires(t *testing.T) {
src := `fn run(s: &str) {
let n: i32 = s.parse().unwrap();
let _v = std::fs::read("x").expect("missing");
let _ = s.parse::<i32>().unwrap_or_else(|_| 0);
let _ = match s.parse::<i32>() { Ok(v) => v, Err(_) => 0 };
}
`
res := runDetector(t, "unsafe-rust-unwrap", "rust", "lib.rs", src)
require.Equal(t, 3, res.Total, ".unwrap(), .expect(), .unwrap_or_else() must fire; the match arm must not")
assert.Equal(t, "warning", res.Matches[0].Severity)
}
func TestDetector_UnsafeRustUnwrap_DoesNotFireOnUnrelatedMethods(t *testing.T) {
src := `fn run() {
let v: Vec<i32> = Vec::new();
let _ = v.iter().count();
let _ = v.len();
}
`
res := runDetector(t, "unsafe-rust-unwrap", "rust", "lib.rs", src)
assert.Equal(t, 0, res.Total)
}
func TestDetector_UnsafeRustPanicMacro_Fires(t *testing.T) {
src := `fn run(n: i32) {
if n < 0 {
panic!("negative");
}
if n == 0 {
todo!("zero handling");
}
if n > 100 {
unreachable!();
}
if n == 1 {
unimplemented!();
}
println!("ok"); // must not match
}
`
res := runDetector(t, "unsafe-rust-panic-macro", "rust", "lib.rs", src)
require.Equal(t, 4, res.Total, "panic!, todo!, unreachable!, unimplemented! must fire; println! must not")
}
func TestDetector_UnsafeRustAssertMacro_Fires(t *testing.T) {
src := `fn run(a: i32, b: i32) {
assert!(a > 0);
assert_eq!(a, b);
assert_ne!(a, -1);
debug_assert!(a < 1_000);
debug_assert_eq!(a, b);
debug_assert_ne!(a, -2);
let _ = format!("{a}"); // must not match
}
`
res := runDetector(t, "unsafe-rust-assert-macro", "rust", "lib.rs", src)
require.Equal(t, 6, res.Total, "all six assert/debug_assert variants must fire")
assert.Equal(t, "info", res.Matches[0].Severity)
}
func TestDetector_UnsafeRustBlock_Fires(t *testing.T) {
src := `unsafe fn raw() {}
fn safe() {
unsafe {
let _ = 0;
}
}
fn also_safe() {
let _ = 1; // no match
}
`
res := runDetector(t, "unsafe-rust-block", "rust", "lib.rs", src)
require.Equal(t, 2, res.Total, "unsafe fn and unsafe { } must each fire once")
}
func TestDetector_UnsafePythonAssert_Fires(t *testing.T) {
src := `def run(x):
assert x > 0
if x < 0:
raise ValueError("neg")
return x + 1
`
res := runDetector(t, "unsafe-python-assert", "python", "lib.py", src)
require.Equal(t, 1, res.Total)
assert.Equal(t, "warning", res.Matches[0].Severity)
}
func TestDetector_UnsafePythonAssert_SkipsTestFiles(t *testing.T) {
src := `def test_run():
assert 1 + 1 == 2
`
res := runDetector(t, "unsafe-python-assert", "python", "test_run.py", src)
assert.Equal(t, 0, res.Total, "test_*.py must be excluded by default")
}
func TestDetector_UnsafeJSThrow_Fires(t *testing.T) {
src := `function run(x) {
if (x < 0) {
throw new Error("neg");
}
return x;
}
`
res := runDetector(t, "unsafe-js-throw", "javascript", "lib.js", src)
require.Equal(t, 1, res.Total)
assert.Equal(t, "info", res.Matches[0].Severity)
}
func TestDetector_UnsafeJSThrow_TypeScript(t *testing.T) {
src := `function run(x: number): number {
if (x < 0) {
throw new Error("neg");
}
return x;
}
`
res := runDetector(t, "unsafe-js-throw", "typescript", "lib.ts", src)
require.Equal(t, 1, res.Total)
}
func TestRawPattern_GoCallExpression(t *testing.T) {
// Sanity test for the raw-pattern path: find every panic()
// call without going through the detector registry.
src := `package x
func F() { panic("x") }
func G() { _ = "panic"; do() }
`
res, err := RunOnSource(context.Background(), Options{
Pattern: `((call_expression function: (identifier) @fn) @match (#eq? @fn "panic"))`,
}, "lib.go", "go", []byte(src))
require.NoError(t, err)
require.Equal(t, 1, res.Total, "only the real panic() call should match; the string literal must not")
}
func TestRawPattern_RejectsBadPattern(t *testing.T) {
_, err := RunOnSource(context.Background(), Options{
Pattern: `(this_node_does_not_exist) @match`,
}, "lib.go", "go", []byte(`package x`))
require.Error(t, err)
}