mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-21 20:18:06 +02:00
[pitboss] phase 21: Track F.1 — SurfaceMap module + Python/Flask vertical
This commit is contained in:
parent
f8bff38217
commit
c03326a658
9 changed files with 1396 additions and 1 deletions
183
tests/surface_flask.rs
Normal file
183
tests/surface_flask.rs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
//! 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:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue