nyx/tests/surface_flask.rs
2026-06-05 10:16:30 -05:00

187 lines
6.3 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Phase 21 — `SurfaceMap` Python + Flask vertical.
//!
//! Five-route Flask fixture exercising:
//!
//! * `@app.route("/", methods=["GET"])` default GET
//! * `@app.route("/submit", methods=["POST"])` POST via methods kwarg
//! * `@app.get("/users")` verb decorator
//! * `@bp.post("/admin")` Blueprint receiver
//! * `@app.route("/secret")` + `@login_required` auth-guarded
//!
//! Asserts every route node appears with the correct `method`, `route`,
//! `auth_required`, and `handler_name`. Round-trips the surface map
//! through SQLite and confirms the byte representation is identical to
//! the in-memory canonical JSON.
use nyx_scanner::commands::index::build_index;
use nyx_scanner::commands::scan::scan_with_index_parallel;
use nyx_scanner::database::index::Indexer;
use nyx_scanner::entry_points::HttpMethod;
use nyx_scanner::surface::{Framework, SurfaceMap, SurfaceNode};
use nyx_scanner::utils::config::{AnalysisMode, Config};
use std::path::Path;
use std::sync::Arc;
fn test_cfg() -> Config {
let mut cfg = Config::default();
cfg.scanner.mode = AnalysisMode::Full;
cfg.scanner.read_vcsignore = false;
cfg.scanner.require_git_to_read_vcsignore = false;
cfg.performance.worker_threads = Some(1);
cfg.performance.batch_size = 8;
cfg.performance.channel_multiplier = 1;
cfg
}
const FIVE_ROUTE_FIXTURE: &str = r#"
from flask import Flask, Blueprint
from flask_login import login_required
app = Flask(__name__)
bp = Blueprint("admin", __name__)
@app.route("/", methods=["GET"])
def index():
return "home"
@app.route("/submit", methods=["POST"])
def submit():
return "ok"
@app.get("/users")
def list_users():
return "users"
@bp.post("/admin")
def admin_create():
return "created"
@login_required
@app.route("/secret")
def secret():
return "shh"
"#;
fn seed_flask_fixture(root: &Path) {
std::fs::write(root.join("app.py"), FIVE_ROUTE_FIXTURE.as_bytes()).unwrap();
}
#[test]
fn surface_map_captures_five_flask_routes() {
let project = tempfile::tempdir().unwrap();
seed_flask_fixture(project.path());
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("surface.sqlite");
build_index("surface", project.path(), &db_path, &test_cfg(), false)
.expect("build_index on flask fixture should succeed");
let pool = Indexer::init(&db_path).expect("re-init pool");
let _ = scan_with_index_parallel(
"surface",
Arc::clone(&pool),
&test_cfg(),
false,
project.path(),
)
.expect("indexed scan should succeed");
let idx = Indexer::from_pool("surface", &pool).expect("from_pool");
let map = idx
.load_surface_map()
.expect("load_surface_map ok")
.expect("surface map persisted after scan");
let entries: Vec<_> = map.entry_points().collect();
assert_eq!(
entries.len(),
5,
"expected five Flask routes, got {entries:#?}",
);
let assert_route = |method: HttpMethod, route: &str, handler: &str, auth: bool| {
let ep = map.entry_for_route(method, route).unwrap_or_else(|| {
panic!("missing route {method:?} {route}; map = {entries:#?}");
});
assert_eq!(
ep.framework,
Framework::Flask,
"framework mismatch on {route}"
);
assert_eq!(ep.handler_name, handler, "handler mismatch on {route}");
assert_eq!(
ep.auth_required, auth,
"auth mismatch on {route} (got {})",
ep.auth_required
);
// Handler location must point inside the project file.
assert!(
ep.handler_location.file.ends_with("app.py"),
"handler location not in app.py: {:?}",
ep.handler_location.file
);
};
assert_route(HttpMethod::GET, "/", "index", false);
assert_route(HttpMethod::POST, "/submit", "submit", false);
assert_route(HttpMethod::GET, "/users", "list_users", false);
assert_route(HttpMethod::POST, "/admin", "admin_create", false);
assert_route(HttpMethod::GET, "/secret", "secret", true);
}
#[test]
fn surface_map_round_trips_byte_identical_through_sqlite() {
let project = tempfile::tempdir().unwrap();
seed_flask_fixture(project.path());
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("rt.sqlite");
build_index("rt", project.path(), &db_path, &test_cfg(), false).expect("first build_index");
let pool = Indexer::init(&db_path).expect("first pool");
let _ = scan_with_index_parallel("rt", Arc::clone(&pool), &test_cfg(), false, project.path())
.expect("first scan");
let idx = Indexer::from_pool("rt", &pool).expect("first from_pool");
let bytes_first = idx
.load_surface_map_bytes()
.expect("load bytes 1")
.expect("surface map persisted 1");
drop(idx);
// Rescan against the same DB. No source change → byte-identical
// canonical surface map.
let _ = scan_with_index_parallel("rt", Arc::clone(&pool), &test_cfg(), false, project.path())
.expect("second scan");
let idx2 = Indexer::from_pool("rt", &pool).expect("second from_pool");
let bytes_second = idx2
.load_surface_map_bytes()
.expect("load bytes 2")
.expect("surface map persisted 2");
assert_eq!(
bytes_first, bytes_second,
"surface_map JSON must be byte-identical across rescans"
);
// Round-trip through the in-memory representation: canonicalise →
// serialise should reproduce the on-disk bytes exactly.
let mut map = SurfaceMap::from_json(&bytes_first).expect("from_json");
let bytes_after_round_trip = map.to_json().expect("to_json");
assert_eq!(
bytes_first, bytes_after_round_trip,
"canonical JSON must match round-tripped JSON"
);
// Light sanity check: the same map deserialised twice still names
// the five fixture routes (i.e. persistence does not lose nodes).
let entries: Vec<&str> = map
.nodes
.iter()
.filter_map(|n| match n {
SurfaceNode::EntryPoint(ep) => Some(ep.route.as_str()),
_ => None,
})
.collect();
for route in ["/", "/submit", "/users", "/admin", "/secret"] {
assert!(
entries.contains(&route),
"route {route} missing after round trip; got {entries:?}",
);
}
}