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

249 lines
7.7 KiB
Go

package contracts
import (
"testing"
"github.com/zzet/gortex/internal/graph"
)
// flaskNode builds a graph node with an explicit kind for the Flask route tests
// (the shared makeNodes helper only emits KindFunction).
func flaskNode(id, name string, kind graph.NodeKind, start int) *graph.Node {
return &graph.Node{ID: id, Name: name, Kind: kind, FilePath: "app.py", StartLine: start, EndLine: start + 2}
}
func flaskFind(cs []Contract, method, path string) *Contract {
for i := range cs {
if cs[i].Meta["method"] == method && cs[i].Meta["path"] == path {
return &cs[i]
}
}
return nil
}
func TestHTTPExtractor_Python_FlaskRestful_SinglePath(t *testing.T) {
src := []byte(`from flask_restful import Api, Resource
class UserResource(Resource):
def get(self, id):
return {}
def post(self):
return {}
api.add_resource(UserResource, '/users/<int:id>')
`)
nodes := []*graph.Node{
flaskNode("app.py::UserResource", "UserResource", graph.KindType, 3),
flaskNode("app.py::UserResource.get", "get", graph.KindMethod, 4),
flaskNode("app.py::UserResource.post", "post", graph.KindMethod, 6),
}
cs := (&HTTPExtractor{}).Extract("app.py", src, nodes, nil)
get := flaskFind(cs, "GET", "/users/{p1}")
post := flaskFind(cs, "POST", "/users/{p1}")
if get == nil || post == nil {
t.Fatalf("expected GET+POST on /users/{p1}, got %+v", contractPaths(cs))
}
if get.SymbolID != "app.py::UserResource.get" {
t.Errorf("GET SymbolID = %q, want the per-verb method node", get.SymbolID)
}
if post.SymbolID != "app.py::UserResource.post" {
t.Errorf("POST SymbolID = %q, want the per-verb method node", post.SymbolID)
}
if get.Meta["framework"] != "flask" {
t.Errorf("framework = %v, want flask", get.Meta["framework"])
}
if names, _ := get.Meta["path_param_names"].([]string); len(names) != 1 || names[0] != "id" {
t.Errorf("path_param_names = %v, want [id]", get.Meta["path_param_names"])
}
}
func TestHTTPExtractor_Python_FlaskRestful_MultiPath(t *testing.T) {
src := []byte(`class ItemResource(Resource):
def get(self): ...
def delete(self): ...
api.add_resource(ItemResource, '/items', '/items/<int:id>')
`)
nodes := []*graph.Node{
flaskNode("app.py::ItemResource", "ItemResource", graph.KindType, 1),
flaskNode("app.py::ItemResource.get", "get", graph.KindMethod, 2),
flaskNode("app.py::ItemResource.delete", "delete", graph.KindMethod, 3),
}
cs := (&HTTPExtractor{}).Extract("app.py", src, nodes, nil)
want := []struct{ m, p string }{
{"GET", "/items"}, {"DELETE", "/items"},
{"GET", "/items/{p1}"}, {"DELETE", "/items/{p1}"},
}
for _, w := range want {
if flaskFind(cs, w.m, w.p) == nil {
t.Errorf("missing %s %s; got %v", w.m, w.p, contractPaths(cs))
}
}
if n := countFlask(cs); n != 4 {
t.Errorf("expected 4 flask contracts, got %d: %v", n, contractPaths(cs))
}
}
func TestHTTPExtractor_Python_FlaskRestful_UnknownClassFallback(t *testing.T) {
src := []byte(`api.add_resource(PlainClass, '/plain')`)
cs := (&HTTPExtractor{}).Extract("app.py", src, nil, nil)
if countFlask(cs) != 1 {
t.Fatalf("expected 1 GET fallback contract, got %v", contractPaths(cs))
}
if flaskFind(cs, "GET", "/plain") == nil {
t.Errorf("expected GET /plain fallback, got %v", contractPaths(cs))
}
}
func TestHTTPExtractor_Python_FlaskRestful_NoVerbMethods(t *testing.T) {
src := []byte(`class HelperResource(Resource):
def helper(self): ...
api.add_resource(HelperResource, '/helper')
`)
nodes := []*graph.Node{
flaskNode("app.py::HelperResource", "HelperResource", graph.KindType, 1),
flaskNode("app.py::HelperResource.helper", "helper", graph.KindMethod, 2),
}
cs := (&HTTPExtractor{}).Extract("app.py", src, nodes, nil)
get := flaskFind(cs, "GET", "/helper")
if get == nil || countFlask(cs) != 1 {
t.Fatalf("expected 1 GET fallback, got %v", contractPaths(cs))
}
if get.SymbolID != "app.py::HelperResource" {
t.Errorf("fallback SymbolID = %q, want the class node", get.SymbolID)
}
}
func TestHTTPExtractor_Python_AddURLRule_ViewFuncMethods(t *testing.T) {
src := []byte(`def handler(): ...
app.add_url_rule('/regular', view_func=handler, methods=['POST', 'PUT'])
`)
nodes := []*graph.Node{flaskNode("app.py::handler", "handler", graph.KindFunction, 1)}
cs := (&HTTPExtractor{}).Extract("app.py", src, nodes, nil)
post := flaskFind(cs, "POST", "/regular")
put := flaskFind(cs, "PUT", "/regular")
if post == nil || put == nil {
t.Fatalf("expected POST+PUT on /regular, got %v", contractPaths(cs))
}
if post.SymbolID != "app.py::handler" {
t.Errorf("POST SymbolID = %q, want app.py::handler", post.SymbolID)
}
}
func TestHTTPExtractor_Python_AddURLRule_DefaultGET(t *testing.T) {
src := []byte(`def h(): ...
app.add_url_rule('/x', view_func=h)
`)
nodes := []*graph.Node{flaskNode("app.py::h", "h", graph.KindFunction, 1)}
cs := (&HTTPExtractor{}).Extract("app.py", src, nodes, nil)
if flaskFind(cs, "GET", "/x") == nil || countFlask(cs) != 1 {
t.Errorf("expected 1 GET /x, got %v", contractPaths(cs))
}
}
func TestHTTPExtractor_Python_AddURLRule_PositionalViewFunc(t *testing.T) {
src := []byte(`def h(): ...
app.add_url_rule('/y', h)
`)
nodes := []*graph.Node{flaskNode("app.py::h", "h", graph.KindFunction, 1)}
cs := (&HTTPExtractor{}).Extract("app.py", src, nodes, nil)
got := flaskFind(cs, "GET", "/y")
if got == nil {
t.Fatalf("expected GET /y, got %v", contractPaths(cs))
}
if got.SymbolID != "app.py::h" {
t.Errorf("SymbolID = %q, want app.py::h", got.SymbolID)
}
}
func TestHTTPExtractor_Python_AddURLRule_NoViewFunc(t *testing.T) {
src := []byte(`app.add_url_rule('/z')`)
cs := (&HTTPExtractor{}).Extract("app.py", src, nil, nil)
if countFlask(cs) != 0 {
t.Errorf("expected 0 contracts without view_func, got %v", contractPaths(cs))
}
}
func TestHTTPExtractor_Python_FlaskRestful_Multiline(t *testing.T) {
src := []byte(`class UserResource(Resource):
def get(self): ...
api.add_resource(
UserResource,
'/users',
)
`)
nodes := []*graph.Node{
flaskNode("app.py::UserResource", "UserResource", graph.KindType, 1),
flaskNode("app.py::UserResource.get", "get", graph.KindMethod, 2),
}
cs := (&HTTPExtractor{}).Extract("app.py", src, nodes, nil)
if flaskFind(cs, "GET", "/users") == nil {
t.Errorf("expected multiline add_resource to resolve GET /users, got %v", contractPaths(cs))
}
}
func countFlask(cs []Contract) int {
n := 0
for _, c := range cs {
if c.Meta["framework"] == "flask" {
n++
}
}
return n
}
func contractPaths(cs []Contract) []string {
var out []string
for _, c := range cs {
out = append(out, c.Meta["method"].(string)+" "+c.Meta["path"].(string))
}
return out
}
func TestFlaskRouteDecorator_MethodsExpansion(t *testing.T) {
src := []byte(`from flask import Flask
app = Flask(__name__)
@app.route('/users', methods=['GET', 'POST'])
def users():
return []
@app.route('/health')
def health():
return 'ok'
`)
nodes := []*graph.Node{
flaskNode("app.py::users", "users", graph.KindFunction, 5),
flaskNode("app.py::health", "health", graph.KindFunction, 9),
}
cs := (&HTTPExtractor{}).Extract("app.py", src, nodes, nil)
get := flaskFind(cs, "GET", "/users")
post := flaskFind(cs, "POST", "/users")
if get == nil || post == nil {
t.Fatalf("expected GET+POST on /users from methods=[...], got %+v", contractPaths(cs))
}
if get.Meta["framework"] != "flask" {
t.Errorf("framework = %v, want flask", get.Meta["framework"])
}
if get.SymbolID != "app.py::users" {
t.Errorf("GET handler = %q, want the wrapped view", get.SymbolID)
}
// @route with no methods defaults to GET only.
h := flaskFind(cs, "GET", "/health")
if h == nil {
t.Fatalf("expected GET /health, got %+v", contractPaths(cs))
}
if h.SymbolID != "app.py::health" {
t.Errorf("health handler = %q", h.SymbolID)
}
if flaskFind(cs, "POST", "/health") != nil {
t.Errorf("@route with no methods must not expose POST")
}
}