Files
codegraph/__tests__/flask-url-rules.test.ts
Colby Mchenry 11cc3d9647 fix(flask): add_url_rule and view_func registrations are routes (#2154)
`bp.add_url_rule('/path', 'name', view_func | positional view, methods=[…])`
and a project's own wrapper that hands a list of paths and a `view_func=`
over (flaskbb's `register_view(auth, routes=[…], view_func=X.as_view(…))`)
now produce route nodes linked to the view. A class-based view is ANY unless
`methods=` says; a function defaults to GET. A call whose path isn't a
string literal (the wrapper's own `add_url_rule(route, …)`) adds nothing.

flaskbb 0 → 103 routes (+104 edges); flask's own tests +27; CTFd, microblog,
bluelog byte-identical.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:03:06 +00:00

75 lines
2.5 KiB
TypeScript

/**
* Flask's imperative registration is a route too: `bp.add_url_rule(…)` with a
* `view_func=` (a function, or a class-based view's `X.as_view('name')`), and
* a project's own wrapper that hands a list of paths and a `view_func=` over
* — flaskbb registers every view through `register_view(bp, routes=[…],
* view_func=…)` and had no routes at all.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-flask-rules-'));
const files: Record<string, string> = {
'requirements.txt': 'flask\n',
'app/helpers.py': `def register_view(bp_or_app, routes, view_func, **kwargs):
for route in routes:
bp_or_app.add_url_rule(route, view_func=view_func, **kwargs)
`,
'app/auth.py': `from flask import Blueprint
from flask.views import MethodView
from app.helpers import register_view
auth = Blueprint("auth", __name__)
class Login(MethodView):
def get(self):
return "form"
def post(self):
return "ok"
def logout():
return "bye"
register_view(auth, routes=["/login", "/signin"], view_func=Login.as_view("login"))
auth.add_url_rule("/logout", "logout", logout, methods=["POST"])
auth.add_url_rule("/health", view_func=logout)
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
describe('Flask add_url_rule routes', () => {
it('names each path and method, and links the view', () => {
const routes = cg.getNodesByKind('route').filter((r) => r.filePath === 'app/auth.py');
const handler = (name: string) => {
const route = routes.find((r) => r.name === name)!;
return cg.getOutgoingEdges(route.id).map((e) => cg.getNode(e.target)!.name);
};
expect(routes.map((r) => r.name).sort()).toEqual(['ANY /login', 'ANY /signin', 'GET /health', 'POST /logout']);
expect(handler('ANY /login')).toContain('Login');
expect(handler('POST /logout')).toContain('logout');
// The wrapper's own `add_url_rule(route, …)` names no path: no route there.
expect(cg.getNodesByKind('route').some((r) => r.filePath === 'app/helpers.py')).toBe(false);
});
});